Skip to main content

Command Palette

Search for a command to run...

Airline Reservation OOD: Stop Treating “Seat” as a Boolean

Updated
3 min read
Airline Reservation OOD: Stop Treating “Seat” as a Boolean

Airline Reservation OOD: Stop Treating “Seat” as a Boolean

Seat state diagram

In interviews and real-world systems alike, one of the most common design mistakes is modeling Seat.availability as a simple boolean (true/false). A seat is not just "free/busy" — it has distinct states, rules for transitions, and business constraints. Treating it as a boolean hides complexity and invites race conditions, double-bookings, and brittle failure handling.

Below is a concise, practical approach to model seat state and enforce safe transitions.

Model seats as stateful entities

Instead of a boolean flag, model a Seat with an explicit status enum and related metadata:

  • Status: Available, Held, Booked (optionally: Blocked, Maintenance, Pending)
  • Hold records: who holds it, when the hold expires, hold id / session id
  • Booking records: booking id, payment state, timestamps, audit trail

This gives you a cleaner domain model and makes it easy to reason about concurrency and failures.

Typical state machine

  • Available -> Held: user starts checkout; create a temporary Hold with an expiry
  • Held -> Booked: payment confirms; atomically convert Hold to a Booking
  • Held -> Available: hold expires or user cancels
  • Booked -> Available: cancellation or refund flow (according to policy)

Enforce transitions through the Booking/Hold APIs rather than letting callers flip a boolean directly.

Implementation notes (practical tips)

  • Create an immutable Hold entity with: hold_id, seat_id, user_id/session_id, created_at, expires_at.
  • When a user begins checkout, insert a Hold and mark seat as Held (or associate hold with seat). The hold should have a short TTL (e.g., 5–15 minutes).
  • Use a single atomic DB transaction when confirming payment to convert the Hold into a Booking. The transaction should:
    • Verify the Hold is still valid (not expired and matches hold_id)
    • Create the Booking record
    • Clear the Hold
    • Update seat status to Booked
  • If payment fails or the gateway is down, explicitly release the Hold (or let the expiry background job release it). Do not rely on eventual cleanup only.
  • Expired holds: run a background job (cron/worker) to remove expired holds and return seats to Available. Emit events if needed.

Concurrency and correctness

  • Naive boolean checks lead to race conditions: two processes can read Available simultaneously and both attempt to book.
  • Use one of these techniques depending on your scale and DB:
    • Optimistic concurrency control (version numbers / CAS) on the seat row and check the Hold id within a transaction.
    • Pessimistic locking (SELECT ... FOR UPDATE) for small-scale systems where contention is low.
    • Dedicated seat allocation service that serializes operations (actor/queue-based) for very high concurrency.
  • Make booking confirmation idempotent: use an idempotency key so retries from the payment system don't create duplicate bookings.

Failure handling and observability

  • Make external failures explicit: if payment gateway is down, the flow should fail gracefully and the Hold should either be released or retried within a bounded window.
  • Keep audit logs: who held the seat, when, why it was released or booked. This simplifies debugging and chargeback disputes.
  • Expose metrics: hold rates, hold expirations, booking success rate, average time from hold->booked.

Why this is better than a boolean

  • Prevents double-booking under concurrency
  • Makes business rules explicit (hold durations, cancellation rules)
  • Simplifies failure handling and retries
  • Provides a clearer audit trail and easier testing

Example (pseudocode)

Transaction confirmBooking(holdId, paymentInfo): hold = SELECT * FROM holds WHERE id = holdId FOR UPDATE if not hold or hold.expires_at < now: throw HoldInvalid charge = PaymentGateway.charge(paymentInfo) if not charge.success: throw PaymentFailed INSERT INTO bookings (seat_id, user_id, ...) VALUES (...) DELETE FROM holds WHERE id = holdId UPDATE seats SET status = 'Booked' WHERE id = hold.seat_id COMMIT

This pattern keeps the critical path atomic and makes the edge cases explicit.


Model seats as a small state machine, not a boolean. It reduces bugs, clarifies behavior, and scales much better when concurrency and external failures are in play.

More from this blog

B

bugfree.ai

361 posts

bugfree.ai is an advanced AI-powered platform designed to help software engineers and data scientist to master system design and behavioral and data interviews.