Most teams pick an isolation level the way they pick a color: by feel. A better procedure is to write down the invariants the data must satisfy at every commit boundary, then work backwards to the weakest level that protects them. An invariant is a statement that must be true of the database whenever no transaction is in flight: "no two active users share an email", "the sum of debits equals the sum of credits", "at least one doctor is on call for each shift".
The SQL standard defines isolation levels in terms of the phenomena they prohibit, from dirty reads up through serialization anomalies the PostgreSQL transaction isolation docs. That framing is convenient for the standard but awkward for application developers, because it describes what the database stops rather than what your code needs. Flipping the direction is what makes the decision tractable: instead of asking "what does repeatable read prevent", ask "which anomaly would violate my invariant, and does this level permit it".
A useful sort order for the anomalies, weakest first:
| Anomaly | What breaks | Typical trigger |
|---|---|---|
| Dirty read | Reads see data that may roll back | Reading uncommitted writes from another session |
| Nonrepeatable read | Two reads of the same row disagree | Business logic that re-reads a value mid-transaction |
| Lost update | One write silently overwrites another | Read-modify-write on a counter or balance |
| Phantom read | A re-run query returns a different row set | Range checks, "count then insert" patterns |
| Write skew | Cross-row constraint violated by two valid-looking transactions | On-call rules, inventory holds, double-booking checks |
| Serialization anomaly | Outcomes impossible under any serial order | Cyclic dependencies between concurrent transactions |
The distinction between anomalous reads and anomalous writes matters more than the table suggests. Write skew is a write anomaly: two transactions each read a set of rows, each makes a change that is locally valid, and the combination violates the invariant. The hospital on-call example is the canonical case, where two doctors each check that at least one other doctor is on call, then both cancel their shift a write-up of write skew on Cockroach Labs' blog. Every transaction is internally consistent; the pair is not.
Decision rule: if an invariant spans more than one row or more than one statement, a lost-update prevention is not enough. You need either a level that detects write skew (serializable), or explicit locking that serializes the conflicting transactions, or a design change that collapses the multi-row check into a single-row constraint the database can enforce for you.



