Skip to content

Concept

Saga Pattern

Multi-service consistency via local transactions — and compensating actions when a step fails.

Saga shows up a lot in high-level design interviews. Once you go database per service, interviewers want to know if you understand what happens to transactions that touch more than one service.

Before saga itself, you need ACID — because saga is basically trying to get that feeling back in a distributed world.

ACID in one line

ACID says: update all, or update none.

Imagine one database, three rows. You update row 1, update row 2, and while updating row 3 something fails. Under ACID, the earlier changes roll back too. Nothing half-committed.

If everything succeeded, then commit — transaction done.

Now ask the hard question: how do you get that behavior when you have multiple microservices, each with its own database, and one user action has to touch more than one of them?

That’s the goal of saga.

What saga means

Saga = sequence of local transactions.

Not one giant distributed ACID transaction. A sequence. Operations happen one after another, usually driven by events on a queue — Kafka or whatever you’re using.

Real example: order, inventory, payment

Online store. Microservices for order, inventory, payment.

User places an order. Order DB updates. Inventory should reduce. Inventory DB updates. Then payment runs — and payment fails.

If you do nothing, order and inventory already changed, payment didn’t. Inconsistent state. Bad.

With saga, on the happy path it looks like this:

  1. Order service updates its DB, then publishes a success message: “I updated.”
  2. Inventory listens, updates stock, publishes: “I updated inventory too.”
  3. Payment listens, completes payment, publishes: “Done.”
  4. User gets success.
Saga — happy path
Order committing
commit step by step · same sequence forwardOrdercommit.InventorywaitingPaymentwaitingUserwaitingOrder → Inventory → Payment → Usereach box: commit… then committed — then the next service starts

Order commits locally → success event → Inventory

A sequence of local transactions — not one distributed ACID lock across services.
Happy path

Order ──success──► Inventory ──success──► Payment ──done──► User ✓

If payment fails after receiving the event, it publishes a failure message. That failure is consumed by inventory in the same kind of sequence success used. Inventory rolls back (compensates) its local change and publishes failure. Order hears that something went wrong, rolls back its change, publishes rollback, and the user gets a failure response.

So saga is not magic ACID across servers. It’s local commits + events + compensating rollbacks when something later fails.

Saga — failure & rollback
Commit Order
commit step by step → fail → rollback step by stepOrdercommitInventorywaitingPaymentwaitingcommitUser · waiting

Order commits locally → success event → Inventory

Commits and rollbacks use the same sequence — forward on success events, reverse on failure events.
Failure + rollback

Order ──success──► Inventory ──success──► Payment ✗
                                              │
                                         failure event
                                              ▼
Order ◄──rollback── Inventory ◄──rollback── (compensate)
  │
User ✗

Interview question they love

Two people: P1 and P2. P1 wants to send $10 to P2.

Balance microservice deducts $10 from P1 (100 → 90). Payment microservice is supposed to record / transfer that $10 to P2 — and that step fails.

What do you do?

Payment publishes a failed event. Balance listens, rolls back, adds $10 back (90 → 100). That’s the saga answer: compensate the earlier local transaction.

Two ways to run a saga

The flow I just described — services publishing success/failure and listening to each other — is choreography. There’s also orchestration, where a central orchestrator drives the sequence.

How you coordinate matters once the flow gets messy. Full breakdown: Choreography vs Orchestration.

When not to use this

Don’t reach for saga if the whole operation fits in one service and one database. Local ACID is simpler and stronger.

Also avoid saga when you can’t define a real compensate step. “Rollback” that isn’t actually reversible (emailed the customer, shipped the package, charged a card you can’t refund cleanly) needs a different design — often human workflows or explicit “undo” business operations, not a fake reverse event.

Production pitfall

Compensating actions that aren’t idempotent. The failure event gets delivered twice (queues do that). Inventory “adds stock back” twice. Now you’ve overcorrected. Every compensate path needs the same discipline as the forward path: safe to retry, or guarded with an idempotency key / saga step status.