Managing data consistency in a microservice architecture using Sagas - Implementing a choreography-based saga
sagas transaction managementThis is the third in a series of posts that expands on my recent MicroCPH talk on Managing data consistency in a microservice architecture using Sagas (slides, video).
The other posts in this series are:
In part 1, I introduced the concept of a saga and part 2, I described two different saga coordination mechanisms:
- choreography - the saga participants collaborate by exchanging events
- orchestration - a centralized orchestrator invokes the saga participants using request/asynchronous responsive
In a later post, I’ll describe how to implement orchestration-based sagas. In this post, I describe the design and implementation of a choreography-based saga.
The choreography-based Create Order
saga
In a choreography-based saga, the saga participants collaborate by exchanging events. Each step of a choreography-based saga updates the database (e.g. an aggregate) and publishes a domain event. The first step of a saga is initiated by a command that’s invoked by an external request, such an HTTP POST. Each subsequent step is triggered by an event emitted by a previous step.
Here is the definition of the choreography-based version of the Create Order
saga:
Step | Triggering event | Participant | Command | Events |
---|---|---|---|---|
1 | - | Order Service |
createPendingOrder() |
OrderCreated |
2 | OrderCreated |
Customer Service |
reserveCredit() |
Credit Reserved , Credit Limit Exceeded |
3a | Credit Reserved |
Order Service |
approveOrder() |
- |
3b | Credit Limit Exceeded |
Order Service |
rejectOrder() |
- |
Each step, except of first, has a triggering event. The step of the saga executes a command, which updates its data and emits an event. Since step 2 can emit one of two possible events, the saga can branch, resulting in steps 3a and 3b.
The following diagram shows the flow:
The flow is as follows:
- The
Order Service
receives thePOST /orders
request and creates anOrder
in aPENDING
state - It then emits an
Order Created
event - The
Customer Service
’s event handler attempts to reserve credit - It then emits an event indicating the outcome
- The
OrderService
’s event handler either approves or rejects theOrder
Implementing a choreography-based saga
Let’s take a look at how the Order Service
and Customer Service
implement the choreography-based Create Order
saga.
This example code is developed using Eventuate Tram, which is a platform that solves the distributed data management problems inherent in a microservice architecture.
The complete example code is in the eventuate-tram-examples-customers-and-orders Github repository.
The Order Service
The saga is initiated in the Order Service
when the client makes a POST /orders
request.
The @Controller
that handles the request calls OrderService.createOrder()
.
@Transactional
public class OrderService {
@Autowired
private DomainEventPublisher domainEventPublisher;
@Autowired
private OrderRepository orderRepository;
public Order createOrder(OrderDetails orderDetails) {
ResultWithEvents<Order> orderWithEvents = Order.createOrder(orderDetails);
Order order = orderWithEvents.result;
orderRepository.save(order);
domainEventPublisher.publish(Order.class, order.getId(), orderWithEvents.events);
return order;
}
The createOrder()
method, which is executed within a Spring-managed transaction persists the newly created Order
in the database using Spring Data for JPA and publishes an OrderCreated
event using the DomainEventPublisher
.
The DomainEventPublisher
class is provided by the Eventuate Tram framework.
It implements the Transactional Outbox pattern and inserts serialized events into a MESSAGE
table.
The Eventuate CDC service then publishes those events to the message broker using either transaction log tailing or polling.
Here is one of the @Configuration
classes for the Order Service
:
@Configuration
@EnableJpaRepositories
@EnableAutoConfiguration
@Import({TramJdbcKafkaConfiguration.class,
TramEventsPublisherConfiguration.class,
TramEventSubscriberConfiguration.class})
public class OrderConfiguration {
@Bean
public OrderService orderService(DomainEventPublisher domainEventPublisher,
OrderRepository orderRepository) {
return new OrderService(domainEventPublisher, orderRepository);
}
This @Configuration
class @Imports
the required Eventuate Tram @Configuration
classes and enables Spring Data for JPA repositories.
It also defines the orderService
@Bean
.
The Order Service
also has event handlers for Customer Service
events.
But rather than look at those, let’s look at the event handlers for the Customer Service
, which are more interesting.
The Customer Service
The Customer Service
subscribes to events published by the Order Service
.
It has event handlers for Order
events including Order Created
and Order Cancelled
.
The following listing shows the event handler for the Order Created
event:
public class OrderEventConsumer {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private CustomerRepository customerRepository;
@Autowired
private DomainEventPublisher domainEventPublisher;
public DomainEventHandlers domainEventHandlers() {
return DomainEventHandlersBuilder
.forAggregateType("io.eventuate.examples.tram.ordersandcustomers.orders.domain.Order")
.onEvent(OrderCreatedEvent.class, this::handleOrderCreatedEventHandler)
.onEvent(OrderCancelledEvent.class, this::handleOrderCancelledEvent)
.build();
}
public void handleOrderCreatedEventHandler(
DomainEventEnvelope<OrderCreatedEvent> domainEventEnvelope) {
Long orderId = Long.parseLong(domainEventEnvelope.getAggregateId());
OrderCreatedEvent orderCreatedEvent = domainEventEnvelope.getEvent();
Long customerId = orderCreatedEvent.getOrderDetails().getCustomerId();
Optional<Customer> possibleCustomer = customerRepository.findById(customerId);
if (!possibleCustomer.isPresent()) {
logger.info("Non-existent customer: {}", customerId);
domainEventPublisher.publish(Customer.class,
customerId,
Collections.singletonList(new CustomerValidationFailedEvent(orderId)));
return;
}
Customer customer = possibleCustomer.get();
try {
customer.reserveCredit(orderId, orderCreatedEvent.getOrderDetails().getOrderTotal());
CustomerCreditReservedEvent customerCreditReservedEvent =
new CustomerCreditReservedEvent(orderId);
domainEventPublisher.publish(Customer.class,
customer.getId(),
Collections.singletonList(customerCreditReservedEvent));
} catch (CustomerCreditLimitExceededException e) {
CustomerCreditReservationFailedEvent customerCreditReservationFailedEvent =
new CustomerCreditReservationFailedEvent(orderId);
domainEventPublisher.publish(Customer.class,
customer.getId(),
Collections.singletonList(customerCreditReservationFailedEvent));
}
}
The event handler:
- Attempts to retrieve the
Customer
from the database and reserve credit - If the
Customer
is not found, it publishes aCustomerValidationFailedEvent
- If credit is successfully reserved, it publishes a
CustomerCreditReservedEvent
- Otherwise, if the credit limit is exceeded it publishes a
CustomerCreditReservationFailedEvent
Here is the Spring @Configuration
class that configures the event handlers.
@Configuration
public class CustomerConfiguration {
@Bean
public OrderEventConsumer orderEventConsumer() {
return new OrderEventConsumer();
}
@Bean
public DomainEventDispatcher
domainEventDispatcher(OrderEventConsumer orderEventConsumer,
DomainEventDispatcherFactory domainEventDispatcherFactory) {
return domainEventDispatcherFactory.make(
"orderServiceEvents", orderEventConsumer.domainEventHandlers());
}
...
This @Configuration
class’s primary responsibility is to subscribe to the Order
domain events.
It does that using the DomainEventDispatcherFactory
, which is provided by Eventuate Tram.
As you have just seen, the Create Order
saga is implemented by a simple exchange of events between the Order Service
and the Customer Service
.
However, while this example is quite straightforward, choreography-based sagas have several drawbacks.
For example, the implementation of the saga is scattered around the participating services, which can make more complex sagas difficult to understand.
Please see my book for a more detailed explanation of the choreography-based sagas.
In a later blog post, I’ll describe how to implement the Create Order
saga using orchestration.
To learn more
- Look at the complete example code in the eventuate-tram-examples-customers-and-orders Github repository.
- Read my Microservices patterns book, which includes a comprehensive discussion of sagas including the benefits and drawbacks of choreography-based sagas.
- Read or watch MicroCPH talk on Managing data consistency in a microservice architecture using Sagas (slides, video)
- Talk to me about my microservices consulting and training services.
- Learn more about microservices at adopt.microservices.io