Vivek Jalondhara. All writing

Backend

Designing Retryable Booking APIs in Node.js

By Vivek Jalondhara · 5 min read
Published

A booking request may succeed even when its response is lost. Give retries a durable identity and a clearly bounded promise.

A customer submits a reservation and the connection closes before the response arrives. The browser cannot tell whether the server committed the booking. Retrying with an ordinary create request can produce a second reservation; refusing all retries leaves the customer unsure what happened.

This educational walkthrough uses the Booking Management System domain. The proposed request contract and storage model are illustrative, not a description of its production payment or reservation infrastructure.

Give one user intention one key

Generate a request key when the customer confirms the booking, and preserve it while retrying that same operation. A second deliberate booking gets a new key, even if its fields look identical. Deriving identity only from the payload confuses a repeated network attempt with another legitimate purchase or reservation.

Scope the stored key to the authenticated actor and operation. A request to create a reservation must not collide with a cancellation using the same text. The key identifies an attempt; it does not authenticate the caller or authorize access to the resulting reservation.

Compare the validated request

Store a representation of the accepted booking input alongside the key. On a repeat request, compare the relevant values before returning the earlier result. If the slot or resource changed, report a key-reuse conflict and require a new intention instead of returning an unrelated reservation.

Compare a validated, normalized structure with a defined contract version. Raw JSON strings differ when property order changes, while an incomplete fingerprint may ignore a field that affects booking behavior. Normalize only values whose equivalence the API actually defines. A changed timezone interpretation, for example, must not disappear behind a convenient string cleanup.

Commit the booking and replay record together

For a short database-only operation, use one transaction to claim the key, enforce reservation rules, create the booking, and save its response. Commit before sending the response. If the response disappears afterward, the next request can find the committed replay record. If the transaction aborts, neither the booking nor its key claim should remain.

The SQL fragment only illustrates the claim. A concurrent insert with the same key can wait on the first transaction. Under Read Committed, a subsequent statement can read the committed winning record after a conflict; do not assume the failed insert returns that record. Apply a bounded wait and return a documented retryable outcome when another attempt is still unresolved.

Protect availability independently

Idempotency prevents a single intention from creating multiple bookings. It does not stop two different customers from reserving the same resource. Availability still needs an authoritative database operation that enforces the chosen slot or interval rule under concurrency.

Test those concerns separately. One test sends the same actor and key twice. Another sends two distinct keys for the same unavailable slot. A successful replay should identify the original reservation, while an availability conflict should explain that the requested booking cannot be made. Merging both into a generic duplicate error makes recovery harder.

Keep external effects outside the promise

An email or provider request cannot be rolled back by a PostgreSQL transaction. Persist a durable instruction for follow-up work with the booking, then deliver it separately. Re-delivery still needs a deduplication policy at the consumer or provider boundary; a unique booking key alone does not guarantee an email is sent exactly once.

A provider timeout is ambiguous too: the remote action may have happened. Use the provider’s documented idempotency and reconciliation features where available. Avoid holding a database transaction open while waiting on that call. The response should distinguish a saved reservation from a follow-up operation that is still pending or needs investigation.

Define replay lifetime and prove recovery

Document how long a key can be replayed and what happens after that window. Deleting replay records turns an old retry into a new operation unless another durable rule rejects it. Tie retention to the application’s recovery needs, and avoid silently promising indefinite duplicate prevention.

Exercise a dropped response after commit, two concurrent attempts, a changed payload, and a database failure before commit. Replaying a result must still pass current authentication and access checks. A useful contract tells the caller which state is known, which is pending, and which action is safe next.

Claim a booking intention inside its transaction

Educational claim fragment. Parameters are bound by the database client. The reservation write and saved response must use the same transaction and connection; this statement is not a complete endpoint.

Illustrative example · SQL · Simplified for explanation

-- Assumes PRIMARY KEY (actor_id, operation, request_key)
-- on request_replays and a NOT NULL request_payload column.
INSERT INTO request_replays (
  actor_id, operation, request_key, request_payload
)
VALUES ($1, 'create-booking', $2, $3::jsonb)
ON CONFLICT (actor_id, operation, request_key) DO NOTHING
RETURNING request_key;

-- A returned row means this transaction claimed the key.
-- No row: read and compare the existing replay in a new statement.
-- Commit the booking and its saved response together.
How the example fits together
  1. Validate and authorize
  2. Claim or replay the request key
  3. Enforce availability and save booking
  4. Commit the response record
  5. Return the known outcome

Conceptual flow for the example above.

Tradeoff to keep in mind

This handles the identity of a database operation. External notifications, payments, retention, locking timeouts, and recovery need their own explicit contracts.

Related project: Booking Management System →

References & further reading

Related posts