Postgres ships with three isolation levels: Read Committed (default), Repeatable Read, Serializable. The default is fine for most workloads — except when it isn't, and the lost-update bug ships to production. Knowing when to escalate isolation is the discipline.
Read Committed — the default
Each statement sees committed data. Two reads in the same transaction can see different values if another transaction committed in between. Fast and concurrent. The right answer 90% of the time.
Repeatable Read — snapshot per transaction
Transaction sees a snapshot at start; all reads return that snapshot. Prevents most anomalies but allows phantoms-via-different-name. Useful for reporting queries within a transaction.
Serializable — strongest
Serializable Snapshot Isolation (SSI). Detects serialization anomalies; aborts conflicting transactions. Higher overhead, occasional abort-retry needed. Use for: anything where you write conditional logic based on a read ('if no one else owns this, take it'). The lost-update bug pattern.
Common bug: read-then-write
Read balance, write new balance. Two concurrent transactions both read 100, both write 90 — one update is lost. Read Committed allows this. Repeatable Read still allows it. Serializable detects and aborts one. Or: use SELECT FOR UPDATE for explicit locking.
Practical guidance
Read Committed: default. Repeatable Read: rarely needed. Serializable: for financial/inventory transactions where lost updates matter. Test under concurrency — staging single-user tests won't reveal lost-update bugs.