MicroservicesData Patternsbeginner
Updated:

Saga Pattern

7 min read

Learn how the saga pattern maintains data consistency across microservices using local transactions and compensating actions, with choreography and orchestration examples.

TL;DR – Quick Answer

The saga pattern keeps data consistent across microservices without a single distributed transaction. It breaks a business workflow into a sequence of local transactions, one per service, and if a step fails it runs compensating transactions to undo the earlier steps. You trade strict atomicity for eventual consistency you design explicitly.

On This Page

The saga pattern answers a question that every microservices system eventually hits: how do you keep data consistent when a single business operation spans several services, each with its own database? In a monolith you wrap the whole thing in one database transaction. Across services, that transaction does not exist.

A saga replaces the one big transaction with a sequence of smaller local transactions — one per service — plus a plan for undoing them if something goes wrong halfway. It is less tidy than a database rollback, and that untidiness is the honest price of splitting your data across services.

Why you cannot just use a transaction

Consider placing an order. Three services are involved: Inventory reserves the stock, Payment charges the card, Order confirms the purchase. Each owns its own database.

A classic ACID transaction cannot span three databases across a network. Two-phase commit can technically do it, but it locks resources across services, scales badly, and blocks everything if the coordinator dies — so real systems avoid it. That leaves you needing another way to make "reserve, charge, confirm" behave as one logical unit. That is the saga.

This problem is a direct consequence of the "each service owns its data" rule from domain-driven design. Clean boundaries give you independence but take away the free consistency a shared database provided.

The core idea: local transactions plus compensations

A saga is a chain. Each service does its own local transaction and then signals success. If a later step fails, the saga runs compensating transactions — explicit actions that undo the earlier committed steps — in reverse order.

The word "undo" is doing a lot of work here. Because the earlier transactions already committed, you cannot roll them back. You issue a new action that reverses them semantically:

  • Inventory reserved stock → compensation releases the reservation.
  • Payment charged the card → compensation issues a refund.
  • Order created as pending → compensation cancels the order.

Pro tip: Design every saga step with its compensation from the start. If you cannot describe how to undo a step, you have found a real design problem — some actions, like "email the customer", cannot be un-sent, so you sequence those to happen only after the risky steps have all succeeded.

Choreography: services react to events

There are two ways to coordinate a saga. The first, choreography, has no central boss. Each service publishes an event when it finishes, and other services react. A message broker like Kafka carries the events — see Apache Kafka basics for the mechanics.

Here is the Payment service reacting to a StockReserved event and publishing its own result:

@Component
public class PaymentSagaHandler {

    private final PaymentService payments;
    private final EventPublisher publisher;

    @KafkaListener(topics = "inventory-events")
    public void on(StockReserved event) {
        try {
            payments.charge(event.orderId(), event.amount());
            // Success -> next service (Order) listens for this
            publisher.publish("payment-events",
                    new PaymentCompleted(event.orderId()));
        } catch (PaymentFailedException ex) {
            // Failure -> Inventory listens and releases the reservation
            publisher.publish("payment-events",
                    new PaymentFailed(event.orderId()));
        }
    }
}

Choreography is simple and decoupled for short flows. Its weakness shows as the saga grows: with seven services reacting to each other's events, no single place describes the workflow, and understanding "what happens when we place an order" means tracing events across the whole system.

Orchestration: a coordinator runs the show

The second style, orchestration, introduces a central orchestrator that owns the workflow. It tells each service what to do and reacts to the result, keeping the sequence in one place:

public class OrderSagaOrchestrator {

    public void placeOrder(OrderContext ctx) {
        try {
            inventory.reserve(ctx.orderId(), ctx.items());
            payment.charge(ctx.orderId(), ctx.amount());
            orders.confirm(ctx.orderId());
        } catch (Exception ex) {
            // Run compensations in reverse for whatever succeeded
            compensate(ctx);
            throw new OrderFailedException(ctx.orderId(), ex);
        }
    }

    private void compensate(OrderContext ctx) {
        orders.cancelQuietly(ctx.orderId());
        payment.refundIfCharged(ctx.orderId());
        inventory.releaseIfReserved(ctx.orderId());
    }
}

The trade-off flips: the workflow is easy to read and debug because it lives in one class, but you now run and maintain an orchestrator, and it must not become a hidden monolith that knows too much about every service. For complex flows, most teams prefer orchestration for exactly this readability.

Eventual consistency is the deal you accept

A saga does not give you the neat all-or-nothing of a database transaction. Between steps, the system is genuinely partially updated — payment taken but order not yet confirmed. You must design for those windows:

  • Make every step and compensation idempotent, because messages get redelivered and you cannot charge a card twice.
  • Handle out-of-order and duplicate events — the network guarantees neither.
  • Show users honest intermediate states ("payment processing") rather than pretending the whole thing is instant.

This is the same eventual-consistency reality that runs through the entire microservices model. Sagas make it manageable; they do not make it disappear.

Common mistake: Writing sagas for operations that never needed to cross services in the first place. If placing an order always touches the same three services in lockstep, ask whether those boundaries are right. A system drowning in sagas is often a sign the domain was split in the wrong places.

The outbox pattern: don't lose events

Sagas built on events hide a subtle bug that trips up almost everyone the first time. In the choreography example, the Payment service does two things: it commits a charge to its own database, and it publishes a PaymentCompleted event. What if the service crashes between those two steps? The card is charged, but no event is ever published, and the saga stalls forever with nothing downstream reacting.

You cannot fix this by publishing first — then a crash leaves an event for a charge that never committed. The database write and the event publish need to be atomic, but they live in two different systems.

The transactional outbox solves it. Instead of publishing directly, the service writes the event into an outbox table in the same local transaction as the business change. A separate process then reads the outbox and publishes to Kafka, marking rows as sent:

@Transactional
public void charge(String orderId, Money amount) {
    payments.debit(orderId, amount);          // business change
    outbox.save(new OutboxEvent(              // same transaction
            "payment-events",
            new PaymentCompleted(orderId)));
}
// A background relay reads unsent outbox rows and publishes them to Kafka

Because both writes share one transaction, they commit or roll back together. The relay guarantees the event eventually reaches Kafka, at-least-once. If you build event-driven sagas, you will almost certainly need the outbox — it is the missing half of "just publish an event".

Observability makes sagas debuggable

A saga spans several services, so when one gets stuck, the failure is not in any single log. You need a correlation id — the orderId works well — stamped on every event and log line so you can reconstruct the whole flow across services. Without it, debugging a half-completed saga means grepping five services and guessing which events belong together. Build this tracing in from the start; retrofitting it during an incident is miserable.

When a saga is the wrong tool

Be honest about the cost. A saga adds real complexity: compensations, idempotency, and partial-failure handling that you would get for free from a single database transaction. If the whole operation fits inside one service, use a local transaction and move on.

Sagas earn their keep only when the workflow genuinely spans services and you have accepted the trade-offs of a distributed system — the same trade-offs weighed in the monolith versus microservices comparison. If most of your operations need sagas, that is a signal to reconsider your boundaries, not to write more sagas.

Interview relevance

"How do you handle a transaction across microservices?" is almost guaranteed in any microservices interview. A strong answer explains why two-phase commit is avoided, describes local transactions plus compensations, and contrasts choreography with orchestration. The detail that impresses is mentioning idempotency and eventual consistency — it shows you know a saga is not a free lunch but a deliberate trade of atomicity for availability.

Semantic locking for in-flight sagas

Because a saga is not isolated like an ACID transaction, other requests can see and act on data that a saga is still working through. Someone could try to cancel an order while its payment saga is mid-flight. A common safeguard is a semantic lock: the saga sets a status like PENDING on the aggregate at the start, and other operations check that status and refuse or defer until the saga reaches a final state. It is not a database lock — it is an application-level flag that encodes "this thing is busy". Designing these intermediate states deliberately, rather than pretending the saga is instantaneous, is what separates a robust saga from one that corrupts data under concurrency.

Sagas lean on messaging and resilience. Study Apache Kafka basics for the event backbone that choreography rides on, and the circuit breaker pattern for keeping each saga step from hanging when a downstream service is unwell.

Frequently Asked Questions

What problem does the saga pattern solve?
It solves consistency across services that each own their own database. Because a single ACID transaction cannot span multiple databases, you cannot atomically reserve stock, charge a card, and confirm an order across three services. A saga chains local transactions and uses compensations to roll back if any step fails.
What is a compensating transaction?
A compensating transaction is an action that semantically undoes a completed step of a saga when a later step fails. If payment succeeded but shipping failed, the compensation issues a refund. It is not a database rollback — the original transaction already committed — so you design an explicit reversing action.
What is the difference between choreography and orchestration sagas?
In choreography, each service reacts to events from others with no central coordinator, which is simple for short flows but hard to follow as it grows. In orchestration, a central orchestrator tells each service what to do and tracks progress, which is easier to reason about and debug but adds a coordinating component.
Does the saga pattern give you ACID transactions?
No. Sagas give eventual consistency, not the isolation and atomicity of a single ACID transaction. There are moments where the system is partially updated — payment taken but order not yet confirmed — so you must design for those intermediate states and make operations idempotent.
When should I not use a saga?
If your operation fits inside one service and one database, use a normal local transaction — a saga is unnecessary complexity. Sagas are for workflows that genuinely span services. If you find yourself writing sagas everywhere, your service boundaries may be wrong and too much logic crosses them.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 15 July 2026 LinkedIn
Chat with us