Building Race-condition-free Reservations with Go and Redis

August 18, 2026

Building Race-condition-free Reservations with Go and Redis

A reservation flow looks simple until two customers select the same seat at nearly the same time. A read followed by a write is not enough: both requests can observe an available seat before either one claims it.

The useful question is not “how do I make this request fast?” It is “which operation establishes ownership, and is that operation atomic?”

Model the workflow explicitly

I use three states for a seat:

  1. Available — no active hold or confirmed booking exists.
  2. Held — one customer owns a short-lived opportunity to complete checkout.
  3. Booked — the reservation is durable and cannot expire.

The hold needs an opaque token. Knowing the seat identifier is not proof of ownership; the caller must present the token returned when the hold was created.

Make acquisition atomic

Redis provides the primitive needed for the critical transition:

SET hold:session-42:A7 <token> NX EX 120

NX writes only when the key does not exist. EX adds a server-side expiry. The availability check and ownership write therefore happen as one operation, so only one concurrent request succeeds.

An application-level mutex is not sufficient once multiple service instances are running. The coordination point must be shared by every instance that can accept the request.

Treat confirmation as a guarded transition

Confirmation must verify that the supplied token still owns the hold before creating the durable booking. A naive GET, compare, then DEL sequence introduces another race. Use a Redis transaction or Lua script so verification and mutation are atomic.

The database should still enforce a unique constraint on the session and seat. Redis coordinates the temporary workflow; the database remains the final authority for durable bookings. Defence in depth matters because timeouts, retries, and partial failures are normal production conditions.

Design retries before they happen

Clients will retry when responses are lost. Hold and confirmation endpoints should therefore be idempotent for the same request identifier. Repeating a successful confirmation should return the existing booking rather than report a misleading conflict.

Useful signals include:

  • hold acquisition conflicts;
  • expired holds;
  • confirmation attempts with invalid tokens;
  • database uniqueness conflicts;
  • time from hold to confirmation.

These metrics explain whether failures come from demand, an unrealistic TTL, abuse, or a broken client workflow.

The broader lesson

Concurrency bugs are usually modelling bugs before they are code bugs. Define ownership, expiry, authority, and retry behaviour first. Once those invariants are explicit, the implementation becomes smaller and much easier to test.

GitHub
LinkedIn