Saga Pattern
Definition
A saga keeps data correct across services without one big transaction. It runs a series of small local transactions, and if a step fails it runs compensating actions to undo the earlier steps.
Why it exists
Each service has its own database, so one business action cannot be a single database transaction. A saga accepts short-lived inconsistency and repairs failures.
How it works
Example: place an order
Steps: create order (pending) → reserve inventory → charge payment → confirm order.
Compensations are business-level, not rollbacks: "release reservation", "refund", "mark cancelled". Some steps cannot be undone (an email already sent); order such steps last, or make the compensation a follow-up message.
Two styles
Choreography
No central controller. Services publish events and react to others' events.
- Pros: simple for 2-4 steps, no single coordinator, loosely coupled.
- Cons: the flow is implicit and spread across services, so it is hard to see, test and change; risk of cyclic dependencies.
Orchestration
A coordinator (the saga orchestrator) tells each participant what to do and tracks state.
public enum SagaState { STARTED, STOCK_RESERVED, PAID, COMPLETED, COMPENSATING, CANCELLED }
public class OrderSaga {
private final String orderId;
private SagaState state = SagaState.STARTED;
public OrderSaga(String orderId) { this.orderId = orderId; }
public SagaState state() { return state; }
/** Returns the next command to send, or null when finished. */
public String on(String event) {
switch (state) {
case STARTED -> {
if (event.equals("OrderCreated")) { return "ReserveStock:" + orderId; }
if (event.equals("StockReserved")) { state = SagaState.STOCK_RESERVED; return "ChargePayment:" + orderId; }
if (event.equals("StockRejected")) { state = SagaState.CANCELLED; return "CancelOrder:" + orderId; }
}
case STOCK_RESERVED -> {
if (event.equals("PaymentSucceeded")) { state = SagaState.COMPLETED; return "ConfirmOrder:" + orderId; }
if (event.equals("PaymentFailed")) { state = SagaState.COMPENSATING; return "ReleaseStock:" + orderId; }
}
case COMPENSATING -> {
if (event.equals("StockReleased")) { state = SagaState.CANCELLED; return "CancelOrder:" + orderId; }
}
default -> { }
}
return null;
}
}
This is the essence: an explicit state machine, persisted after every step, that maps events to commands. Production implementations use frameworks (Temporal, Camunda, Axon, Netflix Conductor, Eventuate Tram) that provide durable state, timers and retries.
- Pros: flow visible in one place, easier to reason about, add steps and handle timeouts.
- Cons: extra component; avoid putting business rules in the orchestrator.
System design considerations
- Persist saga state and use the outbox so state change and message are atomic
- Give every step a timeout and a compensation path
- Expose saga progress for support and monitoring
Making sagas reliable
- Atomically update state and send the message. Use the transactional outbox; otherwise a crash between the database commit and the publish loses an event.
- Idempotent participants. Messages are delivered at least once, so repeated commands must not repeat effects.
- Compensations must be retryable and idempotent, and must not fail permanently.
- Timeouts. A participant that never replies needs a deadline and a compensation path.
- Semantic locks / pending states. Because intermediate states are visible (order is
PENDING), model them explicitly so other flows can see and handle them. Sagas give ACD without I (no isolation); countermeasures include pending flags, commutative updates and version checks.
When to use it, and when not
Use it when
- A business flow spans several services with their own databases
- You can define a meaningful compensation for each step
- Eventual consistency is acceptable for the flow
Think twice when
- A single database transaction could do the job
- A step cannot be undone and cannot be moved last
- You need strict isolation between concurrent flows
Common mistakes
- Publishing events outside the local transaction (dual-write problem).
- Non-idempotent handlers causing double reservations or charges.
- Forgetting timeouts and stuck sagas.
- Orchestrator that contains domain logic from several services.
Interview questions
1. What is the saga pattern and why use it?
A way to maintain data consistency across services using a chain of local transactions and compensating actions, avoiding blocking distributed transactions.
2. Choreography vs orchestration?
Choreography: event-driven, no coordinator, simple but implicit. Orchestration: central coordinator with an explicit state machine, clearer for complex flows. A rule of thumb is choreography for short flows, orchestration beyond about four steps or with branching and timeouts.
3. What is a compensating transaction?
A new transaction that semantically reverses a previously committed step (refund for charge). It is not a database rollback and may leave a visible history.
4. What happens if a compensation fails?
Retry with backoff; make it idempotent; after repeated failure raise an alert for manual intervention. Design compensations so they cannot logically fail.
5. How do you handle lack of isolation?
Use pending/reserved states, optimistic versioning, and design reads to tolerate intermediate states.
6. Why not 2PC/XA?
It holds locks across services and a coordinator failure blocks participants, reducing availability; many modern stores and brokers do not support it.
Try it and check yourself
Try it: run the saga
Run the happy path first, then reset, untick "Payment succeeds" and run it again. The state machine behind this lab has the same transitions as the Java class in the orchestration section.
Set the failures before starting. Untick payment and watch the stock reservation get released.
- Nothing has happened yet. The first event is OrderCreated.
1Payment fails after stock was reserved. What does the saga do?
2Which property do sagas NOT provide?
3A flow has six steps, branching and timeouts. Which style fits best?
Key takeaways
- Saga = local transactions + compensations; eventual consistency is the trade-off.
- Choreography for simple flows, orchestration for complex ones.
- Reliability rests on outbox, idempotency, retries and timeouts.
Related: Outbox and idempotency, CQRS and event sourcing.
Next topicOutbox and idempotencyFinished this page? Track your progress (saved only in your browser).