NAZMUL
← All writing

2026-10-03 · 8 min read

Handling Race Conditions in Seat Selection: A Deep Dive into Travel OTA Systems

A practical guide to protecting flight inventory in a high-concurrency booking flow using database guarantees, short-lived holds, idempotent operations, and reliable recovery.

Flight inventory is deceptively difficult to manage. A search result is a snapshot, not a reservation: availability and price can change while a traveler compares options, enters passenger details, or completes payment. During periods of high demand, multiple customers may attempt to book the same limited inventory at nearly the same time.

The challenge is not simply preventing two requests from updating one database row. An online travel agency (OTA) coordinates its own services with external systems—such as global distribution systems (GDSs), low-cost carrier APIs, and payment providers—over networks where requests can time out, arrive more than once, or succeed without a timely response.

A resilient booking flow must therefore do more than acquire a lock. It must make state transitions safe, handle uncertainty explicitly, and provide a path to reconcile incomplete transactions.

Where the race begins

A typical booking workflow spans several steps:

  1. Search: Retrieve availability and fare information from suppliers, often with a short cache lifetime.
  2. Revalidation: Confirm the selected itinerary and current price with the supplier.
  3. Reservation: Create a booking or hold, such as a passenger name record (PNR), when supported.
  4. Payment: Authorize or collect payment through a payment provider.
  5. Ticketing: Request ticket issuance from the supplier.
  6. Confirmation: Persist the final supplier references and notify the traveler.

Each step can take place in a different service, and the supplier remains the authority on its own inventory. A seat displayed during search may no longer be available by the time ticketing is requested.

Consider two customers selecting the last available seat. Both browser sessions begin with the same search result. If the application treats that result as a guarantee, both may proceed to payment even though only one booking can be fulfilled. A payment can also succeed while the ticketing request fails—or time out after the supplier has actually issued the ticket.

These are separate problems: concurrent access to local state, contention for supplier inventory, and uncertainty about the outcome of remote operations. Each needs an appropriate control.

Establish a source of truth for local reservations

For inventory that the application owns, enforce exclusivity in the database rather than relying on an in-memory check. A transaction can create a short-lived reservation only when the resource is still available. A unique constraint or conditional update provides a final guard against concurrent requests.

For example, a seat-hold record can have a unique key for the inventory unit and a state such as HELD, CONFIRMED, or EXPIRED. The operation that creates a hold should be atomic: either one request claims the unit, or the database rejects the competing request. Application-level checks performed before the transaction are useful for user experience, but are not a concurrency guarantee.

For supplier-managed inventory, a local hold is only an intent to book. It cannot reserve a seat on the supplier's system unless the supplier confirms a reservation or hold. Keep that distinction visible in the data model and user experience; do not represent a local selection as a confirmed supplier allocation.

Use distributed locks carefully

A distributed lock can reduce duplicate work when multiple application instances process requests for the same resource. A key might be scoped to a supplier and itinerary or to a specific booking attempt. Use a unique ownership token when acquiring the lock, and release it only if that token still belongs to the caller. Set a bounded lease so a crashed worker does not block progress indefinitely.

Redis-based locks, including Redlock-style approaches, can be useful coordination tools, but they should not be the only correctness mechanism. Leases can expire while a worker is paused, and network partitions or delayed processes can produce stale work. Keep the database constraint or conditional state transition as the durable guard. Where stale workers could write after a lease expires, use fencing tokens or version checks so an older owner cannot overwrite newer state.

Most importantly, scope the lock to the actual contention point. A broad lock around an entire itinerary can unnecessarily serialize unrelated bookings. A lock is an optimization for coordination; it does not make the supplier's inventory transactional with your own database.

Make every booking attempt idempotent

Retries are normal in distributed systems. A client may resend a request after a timeout, a queue may redeliver a message, or a worker may restart after completing a remote call but before saving its result. Without idempotency, these retries can create duplicate bookings or repeated charges.

Assign each booking attempt a stable identifier and use it as an idempotency key at every boundary that supports one. Persist the key alongside the operation and its result. When the same request arrives again, return the existing outcome or resume the unfinished workflow instead of creating a second operation.

A typical booking record might move through explicit states:

PENDING → REVALIDATING → HELD → PAYMENT_AUTHORIZED → TICKETING → CONFIRMED

Failure and recovery states should be explicit too, for example PAYMENT_FAILED, TICKETING_FAILED, REQUIRES_RECONCILIATION, and CANCELLED. Validate allowed transitions in one place and make transitions conditional on the current state or version. This prevents late or duplicate messages from moving a booking backward or confirming an invalid attempt.

Coordinate payment and ticketing as a workflow

There is no single transaction that atomically commits your database, a payment provider, and an airline supplier. Treat the process as a durable workflow—often implemented as a saga—with clear ownership of each step and a defined response to failure.

When the payment provider supports authorization and later capture, one possible sequence is to authorize funds, request ticket issuance, and capture only after a successful ticket response. If ticketing fails, void the authorization. This can reduce the need for refunds, but it is not appropriate for every supplier, payment method, or booking policy. Some flows require payment before ticketing; in those cases, define refund or compensation behavior and communicate pending outcomes honestly.

Record each remote operation before dispatching it, then record its result with the supplier or provider reference. A transactional outbox can help ensure that a committed state change and its follow-up message are not separated by a process crash. Consumers should still be idempotent: the outbox provides reliable delivery, not exactly-once execution.

Treat timeouts as unknown outcomes

A timeout does not prove that an operation failed. The supplier may have issued the ticket while the response was lost. Retrying immediately with a new request could create a duplicate or conflicting operation.

For uncertain outcomes:

  1. Mark the booking as requiring reconciliation rather than declaring it failed.
  2. Query the supplier or payment provider using the original request or reference, if supported.
  3. Retry only with the same idempotency key when the remote system guarantees idempotent behavior.
  4. Escalate to a controlled manual-review queue when the outcome cannot be confirmed automatically.

Background reconciliation should also inspect bookings that remain in intermediate states beyond their expected processing window. Alerts on these states make operational issues visible before customers report them.

Test the failure paths, not only the happy path

Concurrency and failure tests should cover the cases most likely to break invariants:

  • Two requests attempt to reserve the same locally managed inventory at once.
  • The same booking request is delivered multiple times.
  • A lock expires while its original worker is paused.
  • Payment succeeds but the application times out before recording the response.
  • Ticketing succeeds but the response is lost.
  • A supplier rejects a fare or inventory item during revalidation.
  • A worker restarts between a database commit and message delivery.

Assert outcomes that matter: no duplicate local reservation, no repeated charge, no false confirmation, and a recoverable state for every ambiguous result. Load tests can reveal contention hotspots, but deterministic concurrency tests are often more useful for validating the invariants themselves.

A layered approach is more reliable than a single lock

Race conditions in OTA booking flows cannot be solved by Redis, a database transaction, or retries in isolation. A robust design combines atomic local state transitions, supplier-side revalidation, short-lived holds where available, idempotent requests, carefully scoped coordination, and durable recovery procedures.

The guiding principle is simple: treat search results as temporary, treat remote timeouts as uncertain, and make every transition safe to retry. That approach cannot guarantee that external inventory will always be available, but it can prevent avoidable duplicate work, keep booking state truthful, and give both customers and operations teams a clear path forward when a supplier response is delayed or fails.