Data Consistency in Microservices: Saga and Outbox Explained
Data consistency in microservices means keeping information correct across several services when each one owns its own database. Because no single transaction spans them all, you cannot rely on a simple ROLLBACK. You need patterns such as the saga, the transactional outbox and idempotent consumers instead.
It is also one of the questions that comes up most in architecture interviews and design reviews, and it is easy to get wrong. This guide uses a simple online-store example to explain the problem, compares your three realistic options (two-phase commit, saga and outbox), and finishes with a decision guide, code samples and the mistakes that cause real incidents.
Table of Contents
- Why data consistency in microservices is hard
- Your options at a glance
- Option 1: Two-phase commit (2PC)
- Option 2: The saga pattern
- Option 3: The transactional outbox
- A practical recipe that combines them
- Do you need any of this? A decision guide
- Common mistakes with data consistency in microservices
- Frequently asked questions
Why data consistency in microservices is hard
In a monolith, placing an order is one database transaction. You insert the order, reduce the stock and record the payment. If anything fails, the database rolls everything back and nobody ever sees a half-finished order.
Now split that application into an Order service, an Inventory service and a Payment service, each with its own database. That is good for independence, but the safety net is gone. Every service commits on its own, and the network between them can fail at any moment.

In practice, three kinds of problems appear again and again:
- Partial failure: step 1 and step 2 succeed, step 3 fails, and nothing undoes the first two.
- Lost messages: a service saves its data, crashes before publishing the event, and the rest of the system never hears about it.
- Duplicate messages: the network retries, the same event arrives twice, and a customer is charged twice.
Your options at a glance
Here is how the main choices for data consistency in microservices compare.
| Approach | Consistency | Complexity | Best for |
|---|---|---|---|
| Two-phase commit | Strong, all or nothing | Medium, but fragile in production | A few participants, short transactions, same technology |
| Saga | Eventual, with undo steps | High: every undo must be designed | Business processes that span services |
| Transactional outbox | Reliable event delivery | Medium | Any service that updates its database and publishes an event |
| Avoid the problem | Strong, inside one service | Low | Data that always changes together |
Option 1: Two-phase commit (2PC)
Two-phase commit is the classic answer. A coordinator asks every participant to “prepare”, and only if all say yes does it tell them to “commit”. It gives you true all-or-nothing behaviour.
It is rarely used between microservices, for good reasons. Participants hold locks while they wait for the coordinator, so a slow or crashed coordinator can freeze everyone. Every participant must support distributed transactions, and many message brokers, NoSQL databases and third-party APIs do not. Worst of all, the availability of the whole operation becomes the product of every participant’s availability, which defeats the purpose of independent services.
Option 2: The saga pattern
The saga pattern is the most common answer to data consistency in microservices. Instead of one big transaction, you run a sequence of small local transactions. Each step commits on its own and then triggers the next. If a later step fails, you run compensating actions that undo the earlier steps, newest first.

A few rules make sagas work in real systems:
- Design the undo first. “Release the reserved stock” is easy. “Un-send an email” is impossible, so put steps that cannot be undone at the end of the saga.
- Make every step and every undo idempotent. They will be retried, so running them twice must be safe.
- Never let an undo give up silently. Retry with backoff, and raise an alert if it keeps failing.
Choreography or orchestration?
There are two ways to coordinate a saga. In choreography, services publish events and react to each other’s events with no central controller. In orchestration, a coordinator tells each service what to do next and tracks the state of the whole process.

As a rule of thumb, choreography is fine for two or three services with a simple flow. Once a process has four or more steps, branches or timeouts, an orchestrator is easier to reason about. Tools such as Temporal, Camunda and AWS Step Functions exist for exactly this.
The catch: sagas are not isolated
While a saga is running, other requests can see the in-between state. The order exists, but it is not confirmed yet. To handle this, give records explicit states such as PENDING, CONFIRMED and CANCELLED, show “processing” to users instead of pretending the work is finished, and use a “semantic lock” (a flag that says “this record is being changed by a saga”) when two processes could collide.
Option 3: The transactional outbox
Sagas rely on events, and that exposes a second trap: the dual write problem. When a service saves to its database and then publishes to a broker, those are two separate systems. If the service crashes between the two, you either lose the event or publish one for data that was never saved.
The outbox pattern removes the gap. The service writes the business data and the event into an “outbox” table inside the same local transaction, so they succeed or fail together. A separate relay then reads the outbox and publishes the events to the broker.

The relay can simply poll the outbox table, or it can read the database’s transaction log using change data capture. Debezium is a popular open-source choice for the second approach.
Here is the heart of it in plain SQL:
BEGIN;
INSERT INTO orders (id, status, total)
VALUES (:orderId, 'PENDING', :total);
INSERT INTO outbox (id, aggregate_id, type, payload, created_at)
VALUES (:eventId, :orderId, 'OrderCreated', :json, now());
COMMIT; -- both rows exist, or neither does
The outbox guarantees at-least-once delivery, which means the same event can arrive twice. So every consumer needs to be idempotent:
void onPaymentRequested(Event e) {
if (processedEvents.exists(e.id())) {
return; // already handled, ignore the duplicate
}
payments.charge(e.orderId(), e.amount());
processedEvents.save(e.id()); // commit together with the charge
}
A practical recipe that combines them
These patterns are not rivals. In a healthy system they work together as one stack for data consistency in microservices:
- Draw service boundaries carefully so that data that must change together lives together.
- Use a saga for any business process that crosses services.
- Use an outbox in every service that changes its own data and publishes an event.
- Make every consumer idempotent using unique event IDs.
- Add a correlation ID to every saga so you can trace one order across all services.
- Set timeouts and build a way to find sagas that are stuck.
- Protect the calls themselves with timeouts, retries and the circuit breaker pattern, so one slow service does not stall every saga.
Do you need any of this? A decision guide
Before you reach for any pattern, ask what level of data consistency in microservices your business actually needs.
- Must two pieces of data always agree instantly? They probably belong in the same service, or the same module of a modular monolith. The easiest distributed transaction is the one you never create.
- Does a process cross services and tolerate a few seconds of delay? Use a saga.
- Does a service update its database and send a message? Use the outbox, every time.
- Do you really need strong consistency across a few services? Consider 2PC, but test what happens when the coordinator dies.
- Do you need another service’s data to answer a request? Keep a local copy that is updated by events, and accept that it may be a little stale.
If you find yourself writing sagas for almost every feature, treat it as a warning. Your service boundaries may be in the wrong place.
Common mistakes with data consistency in microservices
- Treating eventual consistency as someone else’s problem. Product, support and the user interface all need to know that some data is temporarily “in progress”.
- Undo steps that are not idempotent. A retried “refund” that refunds twice is worse than the original failure.
- Publishing directly after commit. That is the dual write problem in disguise. Use the outbox.
- Assuming exactly-once delivery. Plan for duplicates and out-of-order messages.
- No timeouts. A saga waiting forever for a reply holds resources and confuses users.
- Hiding intermediate states. Showing “Order confirmed” before payment succeeds causes support tickets.
- Sharing a database “just this once”. It quietly couples services and removes the independence you wanted.
Key takeaways
- Data consistency in microservices is about handling partial failure, lost messages and duplicates, not about finding a magic global transaction.
- Sagas replace one big transaction with local steps and undo steps.
- The outbox makes sure a database change and its event are never separated.
- Idempotent consumers make retries and duplicates harmless.
- Keep data that changes together in the same service whenever you can.
Frequently asked questions
What is data consistency in microservices?
It is the challenge of keeping data correct across services that each own a separate database. Because there is no shared transaction, teams use patterns such as sagas, the transactional outbox and idempotent consumers to reach a consistent state.
Why can’t I just use distributed transactions (2PC)?
You can in some cases, but 2PC holds locks while waiting for a coordinator, depends on every participant supporting it, and reduces overall availability. Most microservice systems prefer sagas.
What is a compensating transaction?
It is an action that undoes the business effect of an earlier step, such as releasing reserved stock or refunding a payment. It does not erase history; it adds a new step that reverses the outcome.
Choreography or orchestration: which should I choose?
Choose choreography for short, simple flows with two or three services. Choose orchestration for longer flows with branches, timeouts or strict monitoring needs.
What problem does the transactional outbox solve?
It solves the dual write problem. By storing the event in the same local transaction as the data change, it guarantees that an event is published if, and only if, the data was saved.
What does idempotent mean in this context?
An operation is idempotent if doing it twice has the same effect as doing it once. Consumers achieve this by recording the IDs of events they have already processed and ignoring repeats.
Further reading
- microservices.io: Saga pattern, the reference description of choreography and orchestration.
- microservices.io: Transactional outbox, the pattern in detail.
- Debezium Outbox Event Router, a practical way to relay outbox events.
Final thoughts
You cannot make a network reliable, but you can design every step so that failure leads to a safe, recoverable state. Start by keeping related data together, add sagas only where a process truly crosses service boundaries, and treat the outbox and idempotency as defaults rather than extras. Do that, and data consistency in microservices becomes a design habit instead of a 2 a.m. emergency.
For more guides like this, browse the Technology & Innovation section.

Leave a Reply