design·lab

Distributed systems

Saga

When a business transaction spans multiple services, you can't use one ACID commit — a Saga runs a chain of local transactions, each with a compensating action to undo it if a later step fails.

✗ The problem

No distributed ACID across services

Booking a trip touches Hotel, Flight and Payment services — each owns its own database. There is no single transaction that can span all three at scale.

// ✗ pretend this is one transaction — it isn't
await hotelDB.reserve(trip);
await flightDB.book(trip);
await paymentDB.charge(trip);  // throws!
// hotel + flight already committed — stuck half-done
🏨 Hotel DB
reserved
✈️ Flight DB
booked
💳 Payment DB
charge fails ✗
If Payment fails after Hotel and Flight already committed, you must not leave the trip half-booked — but a global lock across three independent databases doesn't scale.
✓ How it works

Local transactions + compensating actions

Each step commits locally. If a later step fails, run the compensations for every completed step — in reverse order — to semantically undo them (eventual consistency).

class TripSaga {
  async run(trip) {
    const done = [];
    try {
      done.push(await hotel.reserve(trip));
      done.push(await flight.book(trip));
      done.push(await payment.charge(trip));
    } catch (err) {
      for (const step of done.reverse())
        await step.compensate();
    }
  }
}

Orchestration: a coordinator (above) calls each step and its compensation. Choreography: no coordinator — each service reacts to the previous service's event, and a failure event triggers compensating events.

Reserve Hotel
Book Flight
Charge Card

on failure, compensate in reverse:

Refund
Cancel Flight
Cancel Hotel
✓ See it live

Run the saga — happy path or rollback

Run it end to end, or force the payment step to fail and watch the compensations unwind Flight then Hotel, in reverse order.

Follow the numbered messages top-to-bottom — on failure (red), the Orchestrator compensates completed steps in reverse order: Flight, then Hotel.

✓ Takeaway

Consistency without a global lock

  • No distributed ACID across services — use local transactions plus explicit compensating actions instead.
  • Eventual consistency: the system passes through intermediate states while the saga runs (and while it rolls back).
  • Steps must be idempotent and retryable — network failures mean a step or its compensation may run more than once.
  • Orchestration (central coordinator) vs choreography (event-driven, no coordinator) are the two ways to implement it.
  • Used everywhere: microservice checkout, order fulfillment, trip/booking flows.
🎯 Combines / relates: each step/compensation is a Command; steps flow over queues/events; it's how you keep consistency without a global lock.