Sagas for Java Microservices: Orchestration vs. Choreography
Distributed Transactions: The 2PC Trap
You need to create an order that:
- Reserves inventory
- Charges payment
- Ships the order
If any step fails, you need to undo the previous steps. Two-Phase Commit (2PC) can do this, but it:
- Locks resources across services
- Fails if any participant is unavailable
- Kills performance
Sagas are the modern alternative.

What is a Saga?
A Saga is a sequence of local transactions where each step publishes an event. If a step fails, compensating transactions undo the previous steps.
Example: Order Saga
- Reserve Inventory → Success
- Charge Payment → Fails
- Compensate: Release Inventory → Rollback complete
Orchestration vs. Choreography
Orchestration: Central Coordinator
A single service (orchestrator) controls the workflow.
@Service
public class OrderSagaOrchestrator {
public void createOrder(Order order) {
try {
inventoryService.reserve(order.getItems());
paymentService.charge(order.getTotal());
shippingService.ship(order);
} catch (Exception e) {
// Compensate in reverse order
paymentService.refund(order.getTotal());
inventoryService.release(order.getItems());
throw new SagaFailedException(e);
}
}
}
Pros: Easy to understand, centralized failure handling
Cons: Single point of failure, orchestrator becomes a bottleneck
Choreography: Event-Driven
Services listen to events and react independently.
@Service
public class InventoryService {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
try {
reserveInventory(event.getOrder());
eventBus.publish(new InventoryReservedEvent(event.getOrderId()));
} catch (Exception e) {
eventBus.publish(new InventoryReservationFailedEvent(event.getOrderId()));
}
}
@EventListener
public void onPaymentFailed(PaymentFailedEvent event) {
releaseInventory(event.getOrderId()); // Compensate
}
}
Pros: No single point of failure, scales better
Cons: Harder to debug, eventual consistency
Implementing with Spring State Machine
For orchestration, Spring State Machine is ideal:
@Configuration
@EnableStateMachine
public class OrderSagaConfig extends StateMachineConfigurerAdapter<States, Events> {
@Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states
.withStates()
.initial(States.PENDING)
.state(States.INVENTORY_RESERVED)
.state(States.PAYMENT_CHARGED)
.end(States.COMPLETED)
.end(States.FAILED);
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
transitions
.withExternal()
.source(States.PENDING).target(States.INVENTORY_RESERVED)
.event(Events.RESERVE_INVENTORY)
.action(context -> inventoryService.reserve())
.and()
.withExternal()
.source(States.INVENTORY_RESERVED).target(States.PAYMENT_CHARGED)
.event(Events.CHARGE_PAYMENT)
.action(context -> paymentService.charge())
.and()
.withExternal()
.source(States.PAYMENT_CHARGED).target(States.COMPLETED)
.event(Events.SHIP_ORDER)
.action(context -> shippingService.ship());
}
}
Compensation Strategies
| Failure Point | Compensation |
|---|---|
| Inventory reserved, payment fails | Release inventory |
| Payment charged, shipping fails | Refund payment, release inventory |
| All steps succeed, user cancels | Full rollback via compensations |
When to Choose What
| Pattern | Best For |
|---|---|
| Orchestration | Complex workflows with many conditional branches |
| Choreography | Simple, linear workflows; high-scale event-driven systems |
Conclusion
Sagas sacrifice immediate consistency for availability and performance. By accepting eventual consistency and designing proper compensations, you can build resilient distributed systems without the baggage of 2PC.
Need more distributed patterns? Check out Transactional Outbox or Zero-Downtime Migrations.