</>longpham.tech
All posts
·8 min read

Booking Across Services: Sagas, 2PC, and Overbooking

A hotel stay or a flight itinerary is several resources that must be reserved all-or-nothing, often across different services — the distributed-transaction problem ticketing dodges. A runnable lab on sagas, 2PC's blocking, date-range atomicity, and deliberate overbooking.

#distributed-systems#transactions#saga#architecture

Selling one ticket is a single atomic decrement. Booking a trip is not: a hotel stay needs a room on every night of a range, a flight itinerary needs a seat on every leg — and those resources often live in different services or shards, so there's no shared database transaction to make it all-or-nothing. Reserve leg A, fail on leg B, and leg A is now a charged, held reservation for a trip that can't happen. This is the distributed transaction — the cross-shard write DDIA calls partitioning's Achilles heel — made measurable.

Runnable companion: multi-resource-booking on GitHub. make saga && make twopc && make stay && make overbook reproduces every number below (pure Python). It bridges the inventory labs (ticketing, flash-sale) into distributed transactions.

Reserve every resource or none — the saga

Reserve resources one at a time and a later failure strands the earlier ones. 100,000 bookings, 3 resources each:

per-leg fail   naive orphan reservations   saga orphan reservations
    5%                    13,759                         0
   15%                    34,473                         0
   30%                    50,447                         0

A naive multi-step booking leaks a reservation on every partial failure — money charged, inventory held for a trip that can't happen. A saga reserves each step (each commits locally) and, on failure, runs compensating actions to release the earlier ones → zero orphans: the atomic guarantee without a shared transaction. (If all resources share one database, skip the saga — one ACID transaction is simpler and isolated. The saga is for when they don't.) The price: every forward step needs a correct inverse (release, refund, cancel), and a compensation can itself fail, so it needs retries + a dead-letter path.

Saga vs 2PC: the blocking trade-off

Both give all-or-nothing across services. 2PC (prepare → vote → commit) adds isolation — participants lock, no one sees a half-booking — but a coordinator crash after the prepare leaves them blocked, holding locks until recovery:

coordinator fail   2PC blocked bookings   saga blocked bookings
    2%                     2,017                    0
    5%                     4,918                    0
   10%                     9,965                    0

A saga never waits on a coordinator (commit-or-compensate locally), so it never blocks — trading 2PC's isolation for availability. Rule of thumb: 2PC for co-located, short transactions; sagas for cross-service, long-lived ones — which is most real bookings.

Inventory is per date, and stays are ranges

Hotel/flight stock is keyed by night (per date). A stay reserves a room on every night of its range, all-or-nothing. 10 rooms/night, 30 nights:

stay nights   atomic: stays   naive: stays   naive orphan-nights
    1             213             213                  0
    2             137             132                 21
    3              89              83                 39
    5              49              46                 52

A 1-night stay is a single counter — nothing to strand. From 2 nights up, naive per-night booking leaks room-nights and completes fewer stays; atomic leaks nothing. Longer stays also succeed less often — they need a run of consecutive open nights.

The airline exception: deliberate overbooking

Ticketing must never oversell. Airlines oversell on purpose — no-shows leave paid seats empty. Capacity 200, 10% no-show:

overbook   empty seats   bumped   flights bumping
   0%          20.0        0.00           0.0%
  10%           2.9        0.93          29.5%
  20%           0.0       15.99          99.9%

Overbooking near ~11% (no-show / (1 - no-show)) nearly fills the plane; past that, bumps climb fast, and variance means even the sweet spot bumps sometimes — priced in as bump compensation. Same counter as ticketing, opposite risk appetite: a yield decision, not a correctness one.

What I'd say in an interview

  • Booking a trip is a cross-resource atomic write. Co-located → one ACID transaction. Across services → a saga (reserve + compensate); measured, a naive multi-step booking leaks tens of thousands of orphan reservations.
  • 2PC gives isolation but blocks on coordinator failure; sagas stay available but aren't isolated — pick sagas for cross-service, long-lived flows.
  • Inventory is per date/interval — reserve the whole range atomically; availability falls with stay/route length.
  • Overbooking is a deliberate yield trade-off tuned to the no-show rate — the opposite of ticketing's zero-oversell.