Distributed Data & Consistency
Distributed Data & Consistency
In a monolith every write lands in one database and you get ACID guarantees for free. In a microservices system each service owns its data (Lesson 4), so a single business operation — "place an order" — may need to write to three databases owned by three separate services. There is no distributed transaction manager to wrap them. This lesson explains the consistency model you must design for, and introduces the Saga pattern — the most practical tool for coordinating multi-service writes reliably.
Why Distributed Transactions Do Not Scale
The classic answer to cross-database writes is the Two-Phase Commit (2PC) protocol: a coordinator asks all participants to prepare, waits for their votes, then issues a global commit or rollback. It works, but it has serious costs in a microservices context:
- Blocking locks: between the prepare and commit phases every participant holds database locks. Under high concurrency this becomes a throughput bottleneck.
- Coordinator is a SPOF: if the coordinator crashes after prepare but before commit, participants are stuck in an uncertain state indefinitely.
- Tight coupling: all participants must be reachable at the same moment — contrary to the independent-deployability goal of microservices.
- Protocol support: many modern datastores (NoSQL, managed cloud DBs) do not implement XA / JTA at all.
Eventual Consistency
Eventual consistency is a liveness guarantee: if no new writes arrive, all replicas and services will eventually converge to the same value. It does not mean the system is always wrong or chaotic — it means there is a brief window (milliseconds to seconds in practice) during which different services may see different values.
Designing for eventual consistency requires you to think about three things:
- What is the worst-case stale window? (seconds, minutes?) and is that acceptable for this operation?
- What happens if a step fails partway through a multi-service write? How do you compensate?
- How does the UI handle a state that is temporarily inconsistent? (e.g. show "order pending" rather than a final status)
The Saga Pattern
A Saga is a sequence of local transactions, one per participating service. Each local transaction updates its own database and then publishes an event (or sends a command) to trigger the next step. If any step fails, the saga executes compensating transactions in reverse order to undo the already-completed steps.
Think of it as replacing one big ACID transaction with a chain of small ones, plus a defined rollback path for every step.
Two Saga Coordination Styles
There are two ways to organise the flow:
- Choreography: each service listens for events and reacts autonomously. No central coordinator. Scales well but can be hard to visualise; you have to trace logs across services to understand what happened.
- Orchestration: a dedicated saga orchestrator (a Spring Boot service or a Spring State Machine) explicitly tells each participant what to do next. Easier to reason about and monitor; the orchestrator is a modest coupling point but not a SPOF because it is stateless or stores state in its own DB.
Order-Placement Saga — Choreography Example
Scenario: placing an order requires (1) reserving inventory, (2) charging payment, and (3) creating a shipment record. Each service publishes Spring application events (or Kafka topics in production).
@TransactionalEventListener(phase = AFTER_COMMIT), not @EventListener, when publishing saga events. If you publish inside the same transaction and that transaction later rolls back, the event fires but the DB write did not — leaving downstream services acting on phantom data. AFTER_COMMIT guarantees the local write is durable before the event leaves the process.
Idempotency — The Hidden Requirement
In a distributed system, messages can be delivered more than once (network retries, broker redelivery). Every saga step handler must be idempotent: processing the same event twice must produce the same outcome as processing it once.
Outbox Pattern — Guaranteed Event Delivery
If a service writes to its DB and then publishes to a message broker in two separate operations, a crash between the two leaves the DB updated but the event never sent. The Transactional Outbox pattern solves this: write the event to an outbox table in the same local transaction as the business write, then have a separate relay process (e.g. Debezium CDC, or a Spring @Scheduled poller) forward outbox rows to the broker and delete them.
Consistency Levels in Practice
- Read-your-writes: route a user's reads to the service that just processed their write (sticky routing or version tokens) so they never see stale state for their own actions.
- Monotonic reads: once a client sees a value, subsequent reads must not return an older value. Achieved by version numbers or timestamps in responses.
- Causal consistency: if event A caused event B, any service that sees B must already have seen A. The Saga pattern and event ordering in a Kafka partition provide this within a single order flow.
Summary
Distributed data management trades ACID guarantees for availability and independent deployability. Eventual consistency is the norm; your job is to make the eventual window short and the failure paths explicit. The Saga pattern — whether choreographed or orchestrated — replaces a distributed transaction with a chain of local transactions plus compensating actions. Layer in idempotency guards and the Outbox pattern to make event delivery reliable, and you have the foundations for a data layer that survives partial failures gracefully.