# Two valid requests can still produce an invalid result

Concurrency control protects the rule shared requests depend on. Start with that rule, then choose conditional writes, locks or transaction isolation to enforce it.

By Cobnex editorial. Published 2026-09-10. Updated 2026-09-11.

## Write down what must remain true

A booking service has one remaining place. Two requests each read availability as one and both create a booking. Each request followed the same reasonable sequence, but together they oversold the session.

The important rule is that confirmed bookings must not exceed capacity. Checking availability before writing is insufficient unless the check and the effect are coordinated against competing requests.

Express that rule at the level where it can actually be enforced. A disabled submit button may reduce accidental clicks, but two users, retries and background jobs can still reach the server at the same time.

## Choose a boundary that covers the decision

For a rule held in one row, an atomic conditional update may be enough. More complex rules can span several records and require a transaction strategy that protects the complete decision.

A version check is useful when editing a record based on a previously observed value. A lock can serialise access to a known resource. Stronger transaction isolation can protect broader interactions, with the retry behaviour required by the database.

Do not select a mechanism by habit. Explain which competing operation it prevents and which paths participate in the protocol.

### A reservation protects one shared rule

The availability decision and committed reservation use a coordinated boundary. A competing request receives a defined outcome.

1. **Booking request**: Validate identity, quantity and operation key
2. **Protected decision**: Check capacity within the chosen concurrency boundary
3. **Commit**: Save reservation and related state together
4. **Result**: Confirm success or explain the conflict

## Keep external effects outside speculative work

A transaction may fail and need to run again. Sending an email or charging a provider inside a retried block can repeat an effect that the database cannot roll back.

Persist an intent to perform the external effect as part of the committed workflow, then deliver it through a mechanism with appropriate duplicate handling. The exact implementation depends on the system, but the retry boundary must remain explicit.

Also distinguish a conflict from a duplicate request. Two different users competing for one place is a capacity conflict. The same request arriving twice after a timeout is an idempotency problem. Many business operations need protection against both.

## Design the losing request's experience

A failed conditional write should not become an unexplained server error. For a document edit, preserve the user's draft and show that a newer version exists. For a booking, explain that availability changed before confirmation.

Automatic retries are suitable only when recomputing the operation remains faithful to the user's intent. Silently overwriting another person's edit with the same old form values is not a safe retry.

## Test the interleaving deliberately

Pause two requests after they read the same starting state, then release their writes. Assert the final invariant and each response.

Repeat through every writer, including imports and administrative tools. A concurrency scheme is only as strong as the paths that follow it. The result should remain valid even when one request is rejected, retried or interrupted after commit.

## Sources

- [PostgreSQL: transaction isolation](https://www.postgresql.org/docs/17/transaction-iso.html)
- [PostgreSQL: explicit locking](https://www.postgresql.org/docs/17/explicit-locking.html)
