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

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

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.


