Invental/Review lessons/Check, then act on stale state

9 lessons · 14 checks

Check, then act on stale state

Most race conditions in product code look the same: read a status, decide, then update by ID. Between the read and the write, someone else acts. The fix is to make the database enforce the condition at write time.

Major

Limits that hold by construction

If a limit has to be checked in two places before a lock, two writers can each pass it. Prefer a limit that holds by construction, such as “at most N references per field”, over a computed total that has to be re-checked.

Read the lesson →
Major

Re-check eligibility at write time

When a job is queued from a snapshot of “who is eligible”, re-check eligibility at the moment you write the money row, not only when you build the queue.

Read the lesson →
Major

Put the status check inside the UPDATE

A status check followed by an update by ID is a race; put the status condition into the update's where so the database enforces it at write time.

Read the lesson →
Major

List allowed statuses for every new path

Every new “who may do X” path widens the state machine; list the allowed source statuses explicitly, and where the database cannot enforce “only one pending”, serialise the check with a row lock.

Read the lesson →
Major

Make a column optional without breakage

Make a column optional only after no running code reads it; until then give it a default. And make “fill if empty” writes conditional in the database, not in the browser.

Read the lesson →
Major

Restate the condition in every destructive write

Every destructive or claiming SQL statement should restate the condition it was decided on (still trashed, still the slot I read, still the current run) in its own WHERE clause, because the read that justified it is already stale.

Read the lesson →
Major

Idempotency keys need an order of checks

An idempotency contract needs an order of checks, not just a list of rules. Compare the request fingerprint before looking at the in-flight status, so a changed request never waits behind, or replays, a different one.

Read the lesson →
Blocker

Pin the target before the first await

In a loop that awaits network calls, any reactive “current” value can change between iterations. Capture the target before the first await and pass it down, or the user's navigation will redirect your writes.

Read the lesson →
Blocker

Lock the rows before removing the last owner

“Refuse to remove the last owner” is a concurrency rule, not an if: unless the rows you count are locked in the same transaction as the delete, two admins can each remove “the other” owner at the same moment.

Read the lesson →

The checks, with the lesson behind each

If your app expands user-supplied templates, is the largest possible output bounded by the input rules alone, without a check that a concurrent edit can get around?+
If a limit has to be checked in two places before a lock, two writers can each pass it. Prefer a limit that holds by construction, such as "at most N references per field", over a computed total that has to be re-checked.
Do your scheduled payment or credit jobs re-check the member's current status right before they create the money record?+
When a job is queued from a snapshot of "who is eligible", re-check eligibility at the moment you write the money row, not only when you build the queue.
When you move or edit money records, is the "still editable" condition inside the UPDATE statement, not just in an earlier read?+
A status check followed by an update by ID is a race; put the status condition into the update's where so the database enforces it at write time.
If you add a "re-apply" or "upgrade" path, did you list which current statuses may use it (and test banned users)?+
Every new "who may do X" path widens the state machine; list the allowed source statuses explicitly, and where the database cannot enforce "only one pending", serialise the check with a row lock.
When you stop writing a column, will an older app instance still running during the deploy get null from it?+
Make a column optional only after no running code reads it; until then give it a default. And make "fill if empty" writes conditional in the database, not in the browser.
If your job queue delivers the same message twice, does the job run twice?+
Every destructive or claiming SQL statement should restate the condition it was decided on (still trashed, still the slot I read, still the current run) in its own WHERE clause, because the read that justified it is already stale.
If your API stores responses for replay, are they encrypted or redacted at rest, and is the response size bounded?+
An idempotency contract needs an order of checks, not just a list of rules. Compare the request fingerprint before looking at the in-flight status, so a changed request never waits behind, or replays, a different one.
If a user switches pages while a multi-step save is running, does every remaining step still write to the record they started on?+
In a loop that awaits network calls, any reactive "current" value can change between iterations. Capture the target before the first await and pass it down, or the user's navigation will redirect your writes.
Can two people, clicking at the same time, remove every owner of a workspace in your app?+
"Refuse to remove the last owner" is a concurrency rule, not an if: unless the rows you count are locked in the same transaction as the delete, two admins can each remove "the other" owner at the same moment.

More checks from the same reviews

Yes/no questions from other reviews on this theme. Each one is quick to answer for your own app.

  1. Do your background jobs ever decide that something was deleted based on a read-replica query?
  2. If two teammates accept an invite in the same second, does your billing system charge for both seats?
  3. Do your batch jobs page with offset over rows whose filter columns can change while the job runs?
  4. Can a user change a setting that affects how they get paid (country, bank, tax ID) while a payment to them is being processed?
  5. If your app uses a lock file or “is it already running?” check, can two processes started in the same second both pass it?

Want a senior reviewer to run these checks on your code? Fintech and payments architecture review →

Want these checked on your code?

We run reviews like these on real codebases. Tell us what you are building and we confirm scope before anything starts.

— Get in touch
hi@invental.co ↗
— Or
— What to include

A link or short description of the code or system, what you want checked, your stack, and when you need the answer. No repository access is needed until scope is agreed.

— Or leave a note
We reply within one business day.