How to keep data consistent across services without distributed transactions.
The saga pattern: local transactions and compensating steps, instead of one distributed rollback.
key idea / coordinatorservice / good pathin progress / watch outproblem / failureneutral
The problem: one operation, many databases
In a monolith one ACID transaction keeps everything consistent. With a database per service you need another way.
Sagas instead of 2PC
Two-phase commit needs every participant online and many modern tools do not support it. A saga splits the work into local transactions.
Undoing a saga
A saga cannot simply roll back. It runs compensating transactions for the steps that already committed, in reverse order.
Who runs the saga?
Two styles: choreography (everyone listens to events) and orchestration (one orchestrator tells each service what to do).
Dashed arrows are events. Solid arrows are commands. Replies go back to the orchestrator.
No isolation, so plan for it
Because each step commits at once, other sagas can see half-finished data.
Countermeasures
The most common is the semantic lock: mark records as pending so other sagas know to wait or retry.
What splitting a database costs
ACID means Atomicity, Consistency, Isolation, Durability. A service can still use an ACID transaction on its own database. What you lose is atomicity across services.
So when data must change together, you have four options, from simplest to most work.
Two-phase commit (2PC) is the classic way to get atomicity across databases. It has a vote phase and a commit phase:
2PC problem
Why it hurts
Locks
It is mostly distributed locking: slow, and deadlocks are hard to avoid.
Latency
More participants and slower workers mean longer locks.
Lost isolation
Commits land at different moments, so one worker may show the change before another.
Silent worker
A worker that voted yes then goes quiet can leave a mess an operator must fix by hand.
Rule of thumb. Use 2PC, if ever, only for very short operations. Systems like Google Spanner do it behind one logical database, with very costly infrastructure. In the words of Pat Helland, when every node must work, adding a node lowers availability, like adding an engine to a plane that needs all engines.
From Building Microservices (ch. 6, Workflow) and Decomposing the Database (ch. 4, Transactions).
Transactions: what splitting costs you
ACID means atomicity (all or nothing), consistency, isolation (no half-done reads) and durability. Each local transaction stays ACID after a split, but atomicity across databases is the first thing lost. Many teams then reach for distributed transactions.
Two-phase commit
A coordinator asks all workers to vote; if all agree, it tells them to commit. A worker that votes yes promises to make the change later, so it must lock the record until then.
Problem
Why
Locks
It is mostly distributed locking; long or large operations hold resources a long time.
Failure modes
A worker votes yes then goes silent; recovery can need manual work.
Isolation lost
During commit, one worker may show the change before the other.
Latency grows
More participants and slower networks widen the problem.
Just say no. The first alternative is not to split state that must change atomically; keep it in one database and one service, and split something else first. If you must split, use a saga.
From Monolith to Microservices, ch. 4: "Transactions", "ACID Transactions", "Two-Phase Commits" and "Distributed Transactions: Just Say No".
Failing backward and forward
A saga can recover in two ways. You can mix them in one saga.
A compensating action is a semantic rollback. You cannot unsend an email, so the undo sends a second email saying the order was cancelled. Keep a record of the aborted order, because that history is useful.
From Building Microservices (ch. 6, Saga Failure Modes) and Decomposing the Database (ch. 4, Sagas).
Choosing and mixing saga styles
Choreographed sagas give up the central view. A common fix is a tracker that listens to every event. Each event carries the same correlation id, a unique id created for the saga.
Orchestration
Choreography
Process visible
In one place
Spread across services
Coupling
Orchestrator knows every service
Services only know events
Main risk
Logic drains out of services (anemic services)
Hard to see where a saga is
Usual style
Request-response
Events
Fits when
One team owns the whole saga
Several teams are involved
Keep orchestrators small. Use a different orchestrating service per flow (placing orders, returns, goods receiving) so the shared Warehouse keeps its own logic. If logic can be centralized, it will be. Newman also prefers plain code over drag-and-drop BPM tools, which developers end up using anyway. If you do want a tool, he names Camunda and Zeebe. When mixing styles, still keep a clear way to see a saga's state.
From Building Microservices (ch. 6, Implementing Sagas).
Sagas
A saga breaks a long process into steps, each a local ACID transaction in one service, without holding locks across the whole thing. It does not give saga-level atomicity; it gives you enough state to reason about where you are. Full treatment in the saga sections above.
Failure mode
Meaning
Backward recovery
Undo with compensating transactions (a semantic rollback; you cannot unsend an email, so send a second one).
Forward recovery
Retry from the failure point; needs persisted progress.
Mixed
Early steps roll back; late steps (such as dispatch) retry, then call a person.
Orchestration or choreography
Orchestrated
Choreographed
Control
Central coordinator, request/response
Events on a broker
Visibility
Process modeled in one place
Rebuild it from services; add a correlation ID and a view
Risk
Domain coupling; services become anemic
Hard to see state or trigger compensation
Choose when
One team owns the whole saga
Several teams are involved
Tips. Use a different orchestrator per flow so logic is not all in one place. The author dislikes BPM drag-and-drop tools; developers end up using them anyway, and flows are hard to version and test.
Pat Helland's point: with distributed transactions the more nodes you add, the more likely the whole thing stalls. Modeling the process as a saga also makes the business process explicit.
From Monolith to Microservices, ch. 4: "Sagas", "Saga Failure Modes", "Implementing Sagas" and "Sagas Versus Distributed Transactions".
Living with eventual consistency
Asynchronous events mean one service may read data before another has applied the same event. This stale period is the inconsistency window. Agree with the business where it is acceptable. In CAP terms, event-driven design chooses availability, which pushes stale-data handling onto consumers.
Five ways to deal with it:
Strategy
Gain
Cost
Richer event
No lookup, faster
Bigger events, tied to consumer needs
Domain boundary
Staleness contained inside
Slower to reach outsiders; use sparingly
Version and retry
Quick fix
Load spikes and cascades; use backoff and a circuit breaker
Keep local state
Full autonomy, resilient
Storage cost and upkeep; copies spread
End-to-end check
Parts stay loose and fast
Checkout may still fail late
It is not eventual if nobody notices. One study cited by the book found about 98% of Cassandra reads inside a window of 1 ms or less, and MongoDB at 5 ms or less for 96%. Keep the window small and many users will never see it.
From Practical Event-Driven Microservices Architecture (ch. 5) and Decomposing the Database (ch. 4).
What eventual consistency costs
In a monolith a change is visible everywhere at once. With events, services read stale data until the change propagates. Two properties help you reason about it: safety (nothing invalid happens) and liveness (good things happen, mainly higher availability). Eventual consistency gives no safety guarantee by itself. From Practical Event-Driven Microservices Architecture, ch. 5.
The CAP theorem is often used to justify eventual consistency. The book urges caution: CAP availability is about network partitions, not overall uptime. Google reported only 7.6 percent of Spanner incidents were network related. Eventual consistency also pushes the complexity of stale data onto every client, so confirm the business can live with it. One team lost trust in a product-visibility feature after delays and went back to manual work.
The practical goal is a small inconsistency window. Studies the book cites for Cassandra and MongoDB found windows of a millisecond or a few milliseconds for most requests. If eventual consistency is fast enough, nobody notices. For services that means horizontal scaling and a lag metric.
Five ways to handle stale data
The running example: a subscription service must know the product total stock, but the inventory event only says one XS unit was bought. From Practical Event-Driven Microservices Architecture, ch. 5.
Strategy
Advantages
Limits
1 Denormalized event
Simple, decoupled, no extra request, linear scaling
Larger events; data must exist upstream; meaning can blur
2 Domain boundaries
Little code; flow stays visible; consumers see only settled data
Slower end to end; a read model publishing events is debatable
3 Version and retry
Quick fix: compare event version with API version, retry later
Does not remove the problem; load peaks; needs a circuit breaker
4 Local state
No dependency; fast; resilient; reusable
Storage and upkeep; copies spread across services
5 End to end
Parts stay fast and eventual
Needs a final check in the flow
Local state can also live in memory: either a time window of recent events (good for bulk jobs) or the whole stream loaded at startup (only for small, stable data sets such as a few hundred categories).
Recap
A saga is a series of local transactions linked by messages.
Failures are undone by compensating transactions in reverse order.
Prefer orchestration for anything beyond a simple saga.
Sagas lack isolation. Use countermeasures such as semantic locks.
Use the transactional outbox (chapter 06) so each step's update and message happen together.
Avoid two-phase commit: it holds locks across services. Keep data together if it truly needs ACID.
A saga can fail backward (compensate) or forward (retry). Reorder steps so likely failures come first.
Mix saga styles if needed, and track a choreographed saga with a correlation id.
Event-driven systems are eventually consistent. Use richer events, versions, local state or end-to-end checks.
Splitting tables loses joins, foreign keys and transactions; handle them in code and with sagas, not two-phase commit.
Eventual consistency is manageable: richer events, boundaries, versioned retries, local state, end-to-end checks, and a small inconsistency window kept small by autoscaling on lag.
Quick check
Why can a saga not just ROLLBACK when card authorization fails?Show answer
Each step already committed in its own local database. Rolling back means running new compensating transactions, such as rejectTicket() and rejectOrder().
Payment and loyalty points are done, then packing fails. What now?Show answer
Run compensating transactions (refund, remove points). Better, reorder so points are awarded after dispatch. This is a semantic rollback, not a database rollback.