Skip to main content

Data Consistency in Microservices: Saga and Outbox Explained

Data consistency in microservices: glowing glass service cubes connected in a chain with an amber undo arrow from the failed service back to the first

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

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.

Diagram showing data consistency in microservices going wrong: one order touches three services with separate databases and a payment timeout leaves the data inconsistent
Each service commits to its own database, so a failure halfway through leaves the system in an in-between state.

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.

Saga pattern diagram showing the happy path of an order and the compensating transactions that release stock and cancel the order when payment fails
A saga is a chain of local steps. When one fails, the undo steps run in reverse order.

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.

Comparison diagram of saga choreography with an event bus and saga orchestration with a central coordinator, including the pros and cons of each
Choreography is lightweight but harder to follow. Orchestration is easier to monitor but adds a component to run.

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.

Transactional outbox pattern diagram showing an order and an event saved in one database transaction, then relayed through a broker to idempotent consumers
The order and its event are saved atomically. A relay delivers the event, and consumers ignore duplicates.

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:

  1. Draw service boundaries carefully so that data that must change together lives together.
  2. Use a saga for any business process that crosses services.
  3. Use an outbox in every service that changes its own data and publishes an event.
  4. Make every consumer idempotent using unique event IDs.
  5. Add a correlation ID to every saga so you can trace one order across all services.
  6. Set timeouts and build a way to find sagas that are stuck.
  7. 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

  1. 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”.
  2. Undo steps that are not idempotent. A retried “refund” that refunds twice is worse than the original failure.
  3. Publishing directly after commit. That is the dual write problem in disguise. Use the outbox.
  4. Assuming exactly-once delivery. Plan for duplicates and out-of-order messages.
  5. No timeouts. A saga waiting forever for a reply holds resources and confuses users.
  6. Hiding intermediate states. Showing “Order confirmed” before payment succeeds causes support tickets.
  7. 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

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.

A practical guide for your next challenge. A fresh perspective for everyday life. Discover both through Nandan’s latest articles.

Post Comments

Leave a Reply

Your email address will not be published. Required fields are marked *