Closing Authorization Races Without Locking the Whole Application
Use conditional writes and shared locking protocols to keep security decisions valid when resource state changes concurrently.
A code review can show an authorization check immediately before a sensitive update and still miss a race. The check observes one state; the update acts on another. The important question is whether the decision and the effect share a consistency boundary.
Consider a service that lets a document owner publish a draft. Another request transfers ownership between the permission check and the update. If publication must use the current owner, checking in application memory is insufficient.
Put the invariant in the write
For a rule based entirely on one row, a conditional update can express the requirement directly:
1
2
3
4
5
6
7
8
UPDATE documents
SET status = 'published', version = version + 1
WHERE id = $1
AND tenant_id = $2
AND owner_id = $3
AND status = 'draft'
AND version = $4
RETURNING id, version;
The parameters come from validated request data and authenticated server-side identity. Zero returned rows means the preconditions were not satisfied; the application must not proceed as though publication succeeded. Avoid revealing another tenant’s document existence when mapping that result to an API error.
The version check protects the caller’s expected resource state. The owner and tenant predicates protect the authorization rule. These checks have different purposes even when they live in one statement.
Multi-row rules need a protocol
If authority depends on a membership row, locking only the document is insufficient. One option is a transaction that locks the relevant membership and document rows in a consistent order, verifies the rule, and performs the mutation. Revocation must participate in the same locking protocol. Another option is serializable execution with whole-transaction retries and a clearly defined serial order.
PostgreSQL describes isolation levels and serialization failures in its transaction isolation reference. The application still has to decide what “revoked” means for an operation already in flight. A valid serial ordering can place publication before revocation; stronger user-facing promises may require cancellation or coordination beyond transaction isolation.
Do not hold database locks while waiting on a slow external API. Commit a durable command under the chosen rule, then decide whether execution needs another permission check. That decision should be explicit for delayed or irreversible work.
Make concurrency visible in reviews
I would ask reviewers to write the dangerous interleaving as a short sequence: check, revoke, execute. Then force that ordering in an integration test using barriers, rather than relying on timing sleeps to reproduce it.
The leadership contribution is establishing which invariants deserve this rigor. Money movement, privilege assignment, publication, and data export are good candidates. Requiring the same locking design everywhere creates unnecessary contention and hides the most consequential paths.
Track failed preconditions separately from server failures. A higher conflict rate may indicate legitimate concurrent activity, an awkward workflow, or adversarial retries. The team needs enough context to distinguish those cases without logging confidential document contents.