Lesson 1 of 4 · 60 min

A successful request is not the invariant

Trace a duplicate reservation and design one durable operation record.

An endpoint can return a successful response while the business operation is wrong. Two successful reservations for the final seat are still a failure. Start with a statement about stored state. For a workshop with capacity one, the number of active reservations must never exceed one. That statement remains meaningful when browsers retry, application processes restart, or the response disappears.
An invariant needs an owner. If an application checks capacity and later inserts a reservation, another process can change the same data between those steps. A check in memory protects only the current execution. The database must decide which operation wins. Model the thing that must be unique, such as the allocation of a specific seat, and give it an appropriate uniqueness constraint. Capacity pools need a conditional update or another concurrency strategy, rather than a unique user identifier.
Now separate request identity from operation identity. A transport request gets a fresh trace identifier each time. A user action gets one operation identifier that survives retries. The server scopes that identifier to the authenticated account and operation type. It also compares the submitted parameters with those of the original operation. Otherwise an accidental key reuse could return the outcome of a different purchase.
Stripe documents the behavior of its idempotency keys, including result reuse and parameter comparison. That is provider-specific behavior. An application must define its own retention period, concurrent-request behavior, and durable storage. A random key held only in process memory disappears with the process. A key held forever can create storage and privacy costs. The contract must state what a caller can safely do after expiry.

Worked example

A fictional class has seat S7. Account A sends operation K9 to reserve S7. Request R1 claims the operation record and seat in one database transaction, stores reservation B42 as the result, and commits. The network drops the reply. Request R2 carries K9 and the same parameters. It returns B42 and does not allocate another seat. Request R3 carries K9 with seat S8. It receives a key-conflict error. A second account using K9 does not read A's operation because the lookup includes account identity.
The completed state contains one reservation, one operation record, and two transport attempts for the same action. The response code alone does not establish this outcome. Inspect the durable identifiers.

Inspect the transaction, not only the endpoint

The following is a teaching schema for assigned seats. Its operations table is dedicated to the reserve-seat operation type; a shared table for multiple operation types must include operation_type in its key and lookup. It is PostgreSQL-oriented SQL, not a complete production migration. The reservation constraint expresses one active allocation per event and seat. A production model that permits cancellation must decide whether to retain historical reservations in a separate table or use an appropriate partial uniqueness rule. Do not remove history merely to make a constraint convenient.
sql
1CREATE TABLE reservations (2	reservation_id text PRIMARY KEY,3	account_id text NOT NULL,4	event_id text NOT NULL,5	seat_id text NOT NULL,6	UNIQUE (event_id, seat_id)7);8CREATE TABLE operations (9	account_id text NOT NULL,10	operation_key text NOT NULL,11	request_fingerprint text NOT NULL,12	reservation_id text NOT NULL,13	PRIMARY KEY (account_id, operation_key)14);
The operation record and reservation belong in the same transaction. A unique operation key alone does not ensure that a seat is unique. A unique seat alone does not tell a caller whether its own timed-out request won. These two constraints answer different questions. Keep both rules explicit and give the application a defined response for each conflict.
The request fingerprint represents the canonical business parameters, including event, seat, and any price or currency that is part of the operation contract. Do not hash raw JSON bytes without considering equivalent representations. Property order and insignificant formatting can differ while intent remains the same. Conversely, omitting currency from the fingerprint can make different purchases look identical. The precise canonicalization belongs in a tested contract.

A second failure schedule

Two requests for the same account and key can arrive concurrently before either has a result. The first transaction attempts to create the operation and reservation. The second conflicts on the operation identity. Its correct next action depends on the database transaction outcome and application contract. It may wait for the first transaction to commit, then read and return the result. It may return a documented operation-in-progress response that the client can poll. It must not bypass the conflict and perform the reservation under a different key.
StepRequest R1Request R2Durable result
1Begins transaction for K9Begins same K9None
2Claims K9 and S7Encounters K9 conflictR1 uncommitted
3Commits B42Waits or returns pendingK9 maps to B42
4Reply disappearsReads completed K9Same B42
If R1 rolls back instead, R2 may become the successful owner after the database resolves the conflict. The application must not treat every uniqueness error as proof of a completed prior operation. Read the committed record and distinguish an operation conflict from a seat already allocated by a different operation.
Now introduce a server crash after the transaction commits but before an in-memory cache updates. The database still contains B42 and K9. Recovery must read authority rather than treating the cold cache as absence. This is why a cache can accelerate an idempotency lookup but cannot be the sole evidence of a committed effect unless it is itself the durable authority by design.

Misconceptions to correct

A tempting approach is to write an operation marker first, commit it, then reserve the seat. This prevents duplicate starts but creates a new stuck state when the process dies between the marker and reservation. Such a design can work only with an explicit recoverable state machine and ownership protocol. It is not equivalent to the atomic transaction shown here.
Another tempting approach is to delete the operation record after sending a successful reply. The server cannot know that the client received and persisted the reply. Deleting the record immediately reopens the ambiguity on the next retry. Retention must cover the documented retry window and account for storage and privacy requirements.

Extend the exercise

Use the table to explain a third request that has the same key but a different seat. The model answer compares the fingerprint and rejects the conflict, even if the new seat is available. Then explain a new key for the same already-reserved seat. That request conflicts with seat allocation rather than operation replay. Award two additional points for distinguishing these outcomes and one for saying which durable record provides the evidence. The goal is to connect API behavior to stored invariants, not memorize a generic duplicate-error response.

Exercise and solution

Change the scenario so R1 crashes before commit. State what R2 should observe. Then change it so R1 commits but crashes before sending the reply. The first case leaves neither allocation nor completed operation. R2 can perform the transaction. The second case leaves both, so R2 returns B42. Award one point for each correct state and one for rejecting changed parameters. A solution that records the key before the business write without recovery logic loses the third point.

Interview probe and wrap-up

Why is disabling the submit button insufficient? A strong answer names retries, multiple devices, and requests that bypass the interface. Follow up by asking how long K9 remains valid. A weak answer says that POST is automatically idempotent. Write down the invariant, operation identity, and durable result before choosing an endpoint name.

Sources

docsStripe idempotent request contractdocs.stripe.comdocsPostgreSQL constraintspostgresql.org

Checkpoint

Which constraint answers whether two attempts represent the same account action?

APrimary key on reservation ID only.BUnique request trace ID.CUnique account and operation key.DUnique event and seat.
Sign up free to answer and see why

Checkpoint

R1 commits, then the response is lost and the cache is empty. What should retry consult?

AThe durable operation record.BA new key generated by the server.CA replica read that is allowed to lag behind the committed write.DThe cache only and create on miss.
Sign up free to answer and see why

Checkpoint

Same key, changed currency, same amount. Which fingerprint policy is valid?

AIgnore changed parameters after success.BCompare amount only.CHash the raw JSON bytes without canonicalizing equivalent representations.DInclude all business-significant parameters in canonical form.
Sign up free to answer and see why

Checkpoint

R2 conflicts on an uncommitted operation marker. What is safe?

ATreat the conflict as a permanent seat denial.BResolve committed state or return documented pending status.CAssume success immediately.DSkip the marker and reserve separately.
Sign up free to answer and see why

Checkpoint

A new operation key requests an already-allocated seat. What explains rejection?

AThe retention period of the earlier operation key.BA provider retry budget exhausted before allocation.CThe seat allocation invariant.DA conflict on the earlier account's operation key.
Sign up free to answer and see why

Can you explain why the operation key and seat constraint protect different rules, then trace both a lost reply and a concurrent retry without creating another reservation? State the relevant identifiers, failure boundary, and evidence in your own words before selecting your confidence.

Not yetGetting thereConfident

Sources

Free to read · better with Enzo

Learn it with Enzo

Save your progress, answer the checkpoints, and let Enzo quiz you on what you just read.