Eventuate Tram Sagas
The Eventuate Tram Saga framework is a saga orchestration framework for Java microservices that use JDBC/JPA. A saga is a mechanism for maintaining data consistency across multiple services in microservice architecture without using distributed transactions. A saga consists of a series of a local transactions.
There are two different ways of coordinating a saga:
-
choreography - the saga’s participants exchange events
-
orchestration - a centralized orchestrator uses request/asynchronous reply-style messaging to tell the participants what to do
Choreography works well for simple sagas but for more complex sagas, orchestration is often easier to understand.
Eventuate Tram Saga is described in more detail in my book Microservice Patterns. It is built on the Eventuate Tram framework, which enables an application to atomically update a database and publish a message without using JTA.
Using Eventuate Tram Saga
There are four parts to using Eventuate Tram Saga to implement an orchestration-based saga:
-
Specifying Maven/Gradle dependencies
-
Writing the saga orchestrator
-
Writing each saga participant
-
Running the Eventuate Tram CDC service
Maven/Gradle artifacts
The artifacts are in JCenter. The latest version is:
If you are writing a Saga orchestrator add this dependency to your project:
-
io.eventuate.tram.sagas:eventuate-tram-sagas-simple-dsl:$eventuateTramSagasVersion
If you are writing a saga participant then add this dependency:
-
io.eventuate.tram.sagas:eventuate-jpa-sagas-framework:$eventuateTramSagasVersion
You must also include one of the Eventuate Tram 'implementation' artifacts:
-
io.eventuate.tram.core:eventuate-tram-jdbc-kafka:$eventuateTramVersion
- JDBC database and Apache Kafka message broker -
io.eventuate.tram.core:eventuate-tram-in-memory:$eventuateTramVersion
- In-memory JDBC database and in-memory messaging for testing
Writing an orchestrator
The Customers and Orders uses a saga to create an Order
in the Order Service
and reserve credit in the Customer Service
.
The CreateOrderSaga
consists of the following three steps:
-
The
CreateOrderSaga
is instantiated after theOrder
is created. Consequently, the first step is simply a compensating transaction, which is executed in the credit cannot be reserved to reject the order. -
Requests the
CustomerService
to reserve credit for the order. If the reservation is success, the next step is executed. Otherwise, the compensating transactions are executed to roll back the saga. -
Approves the order, if the credit is reserved.
Writing the saga orchestrator class
Here is part of the definition of CreateOrderSaga
.
public class CreateOrderSaga implements SimpleSaga<CreateOrderSagaData> {
private SagaDefinition<CreateOrderSagaData> sagaDefinition =
step()
.withCompensation(this::reject)
.step()
.invokeParticipant(this::reserveCredit)
.step()
.invokeParticipant(this::approve)
.build();
@Override
public SagaDefinition<CreateOrderSagaData> getSagaDefinition() {
return this.sagaDefinition;
}
private CommandWithDestination reserveCredit(CreateOrderSagaData data) {
long orderId = data.getOrderId();
Long customerId = data.getOrderDetails().getCustomerId();
Money orderTotal = data.getOrderDetails().getOrderTotal();
return send(new ReserveCreditCommand(customerId, orderId, orderTotal))
.to("customerService")
.build();
...
The reserveCredit()
creates a message to send to the Customer Service
to reserve credit.
Creating an saga orchestrator
The OrderService
creates the saga:
public class OrderService {
@Autowired
private SagaManager<CreateOrderSagaData> createOrderSagaManager;
@Autowired
private OrderRepository orderRepository;
@Transactional
public Order createOrder(OrderDetails orderDetails) {
ResultWithEvents<Order> oe = Order.createOrder(orderDetails);
Order order = oe.result;
orderRepository.save(order);
CreateOrderSagaData data = new CreateOrderSagaData(order.getId(), orderDetails);
createOrderSagaManager.create(data, Order.class, order.getId());
return order;
}
}
Writing a saga participant
Here is the CustomerCommandHandler
, which handles the command to reserve credit:
public class CustomerCommandHandler {
@Autowired
private CustomerRepository customerRepository;
public CommandHandlers commandHandlerDefinitions() {
return SagaCommandHandlersBuilder
.fromChannel("customerService")
.onMessage(ReserveCreditCommand.class, this::reserveCredit)
.build();
}
public Message reserveCredit(CommandMessage<ReserveCreditCommand> cm) {
...
}
...
Running the CDC service
In addition to a database and message broker, you will need to run the Eventuate Tram CDC service. It reads messages and events inserted into the database and publishes them to Apache Kafka. It is written using Spring Boot. The easiest way to run this service during development is to use Docker Compose. The Eventuate Tram Code Basic examples project has an example docker-compose.yml file.