One book, one database, two buyers clicking "purchase" at the same instant. Everything in this
chapter — isolation levels, locks, snapshot isolation — exists to make sure exactly one of them succeeds and the
data never lies.
🧭
How to read this
This opens a new, advanced course — Designing Modern Web-Scale Distributed Services. It starts where
it must: getting concurrency right on a single database before scaling to many. The thread is
one bookstore example carried throughout — first the problem (why a code lock fails across servers), then the
four anomalies isolation must prevent, the four isolation levels ranked by strictness, the locks and snapshots
that implement them, and finally the optimistic-vs-pessimistic choice. Anything marked [+] is context I added; everything else is rewritten from the three teaching lessons.
01
A code-level lock breaks the moment you scale out
The naive fix for concurrent access is a language keyword — and it works right up until you add a
second server. Picture an online bookstore on a single database, and one book with one copy left as
multiple customers try to buy it. The tempting solution is Java's synchronized keyword on the
purchase method:
public synchronized voidbuyBook() { // book purchase code }
synchronized ensures only one request thread enters buyBook() at a time; other
concurrent buyers' threads wait until the first transaction completes. That handles the race — with one catch.
⚠️
The lock only covers one JVM
synchronized obtains its lock within a single JVM instance. Run the app
across multiple servers behind a load balancer and it fails — each JVM locks independently, so two servers
can both enter buyBook() at once. You'd need a distributed lock across JVMs. The better answer:
let the database handle concurrent access via transaction isolation levels.
Figure 1 — The synchronized lock is per-JVM. Behind a load balancer, each server
locks independently, so both reach the single database concurrently — the coordination has to move into the
DB. [+] Diagram of the lesson's multi-JVM failure.
Isolation is the "I" in ACID. An ACID transaction executes without affecting other
transactions, ending in a durable, consistent state — and rolling back entirely on any failure. Every
ACID-compliant database offers transaction isolation levels (each with its own implementation)
so the database, not your code, keeps concurrent transactions isolated enough to stay consistent.
02
The four ways concurrent reads go wrong
Isolation levels are defined by which of four anomalies they prevent. All four leave the database showing
something false.
Anomaly
What happens (bookstore example)
Dirty read
T1 updates a row but hasn't committed; T2 reads that uncommitted value and sends it to the UI. Then T1
rolls back or changes it — T2 already delivered wrong data. A clerk bills a customer for an order
the salesperson hasn't finalized (or later cancels).
Lost update
T1 reads count 10, sells 2 → writes 8. Concurrently T2 also read 10, sells 4 → writes 6. T1's update
is lost; the true count should be 4.
Non-repeatable read
T1 reads a book's count as 4 and shows it; before checkout, T2 buys 2; T1 re-reads and now sees 2. The
same read gave two different values within one transaction.
Phantom read
T1 runs a search matching a criteria; T2 concurrently inserts rows matching that same criteria; T1
re-runs the search and new "phantom" rows appear that weren't in the first result set.
🔑
The distinction that trips people up
Non-repeatable read is about an existing row's value changing between two reads.
Phantom read is about the set of rows changing — new rows appearing that match
your query. That difference is exactly what separates the two middle isolation levels. [+]
03
Four isolation levels, strictest to loosest
The databases offer four isolation levels; each looser level permits one more anomaly in exchange for
better performance. Here's the whole ladder at a glance:
Level
Dirty read
Lost update
Non-repeatable
Phantom
Serializable(strictest)
✕ prevented
✕
✕
✕
Repeatable Reads
✕
✕
✕
⚠ possible
Read Committed
✕
⚠
⚠
⚠
Read Uncommitted(loosest)
⚠
⚠
⚠
⚠
Serializable — the strictest
Guarantees concurrent transactions behave as if they ran serially, one after another. It
prevents all four anomalies. The cost is performance: the database locks resources so only one transaction
touches a resource at a time, which cuts throughput.
Repeatable Reads
A transaction gets the same value on repeated reads despite concurrent updates. T1 reads a
page's comment count as 20; T2 adds two (→22); T1's second read still returns 20. Prevents dirty reads, lost
updates, and non-repeatable reads — but phantom reads are still possible (new matching rows can
appear).
Read Committed
A transaction sees the latest committed value on each read. Same example: T1 reads 20, T2
commits +2, T1's second read now returns 22. Prevents only dirty reads; lost updates, non-repeatable reads, and
phantoms are all still possible. Less strict than Repeatable Reads.
Read Uncommitted — the loosest
Transactions aren't isolated from each other at all — all four anomalies can occur, including dirty reads.
Mostly useful for read-only use cases where inconsistency doesn't matter.
🎚️
Which level to pick depends on two things
(1) Business requirements and (2) the database's specific implementation
— isolation levels have subtle differences between databases. Confirm your choice by reading the DB's
documentation and checking its concurrency-test results for your specific scenarios.
04
How it's enforced: locks and snapshots
Databases implement isolation with read and write locks on resources — applied at row, table,
or whole-database scope. The bigger the lock's span, the bigger the performance hit.
Shared lock (read lock)Held by multiple transactions at once, for reads. While
held, the object can't be modified — preventing other transactions from picking up partial, inconsistent
updates (critical in stock trading or finance).
Exclusive lock (write lock)Held by only one transaction, for writes. No one else
can read or write that object until it's released.
Why even lock reads? A read lock stops other transactions from seeing a partial update
mid-flight — like the salesperson/clerk billing example, where reading an uncommitted order caused a wrong bill.
But locking every read is a heavy performance overhead. The fix is snapshots.
Snapshot isolation
Instead of locking, give every transaction its own snapshot of the record it reads or writes.
Writes happen on the transaction's private snapshot version, not the real record. At commit, the update is
applied to the actual record only if no other concurrent transaction made a conflicting update to it —
otherwise the losing transaction is aborted and must retry.
Figure 2 — Snapshot isolation: each transaction writes to a private copy; at merge only the
conflict-free transaction commits, the other aborts and retries. Original diagram after the lesson's
description.
The upside is on reads — new transactions never wait for read locks, so everyone gets a
consistent snapshot and read-only transactions never abort. The downsides: it offers too
much isolation when transactions need to see each other's updates quickly (they're working on separate
versions merged later), and holding a version per transaction is memory-intensive.
✈️
When snapshots are the wrong choice
With no write locks, n users could each book the same flight seat on their own snapshot — only to
be told at merge that their transaction failed. Same danger in banking transactions on one account. For
these, locks are a must. Also: every DB implements isolation differently, and concurrency
bugs mostly surface in production — they're hard to reproduce in a dev environment, so test
concurrent scenarios thoroughly.
05
Optimistic vs. pessimistic concurrency control
The two implementation strategies come down to a single assumption: how many write conflicts do you
expect?
Optimistic
Pessimistic
Assumes
Few/no write conflicts.
Many write conflicts.
Uses locks?
No — discourages locks; transactions work on snapshots and merge.
Yes — uses DB locks to serialize access.
Implemented by
Snapshot isolation.
Locking (e.g. serializable via read/write locks).
Trade-off
Better performance; conflicts cause aborts/retries at merge.
Guaranteed consistency; performance hit from locking.
Pick when
Conflicts are rare.
Conflicts are common (flights, banking).
This restates the chapter's central tension: serializable + locks guarantees consistency but
hurts performance; snapshot isolation needs no locks and performs better but breaks down under
many write conflicts. Match the strategy to your conflict rate.
➡️
Where the course goes next
Everything here assumed a single database instance. The next chapter asks what happens
when load outgrows one instance — either the data swells beyond one server or traffic spikes beyond what one
DB can serve — which opens the door to partitioning and replication.
06
Recap
Code locks don't scalesynchronized is per-JVM; behind a load
balancer it fails. Push concurrency control into the database.
Four anomaliesDirty read, lost update, non-repeatable read, phantom read — each
leaves the DB showing something false.
Locks: shared vs. exclusiveRead locks (shared, block writes) and write locks
(exclusive). Bigger lock span = bigger performance hit.
Snapshot isolationPrivate per-transaction versions, merged if conflict-free. Fast
reads, memory-heavy, unsafe under many write conflicts.
Optimistic vs. pessimisticBet on few conflicts (snapshots, no locks) vs. many
conflicts (locks, guaranteed consistency).
💡
The single idea underneath it all
Consistency and performance pull in opposite directions, and isolation level is the dial between them.
Every stricter level buys correctness by giving up throughput; every looser level buys speed by permitting
one more way for the data to lie. The whole skill is knowing exactly how much lying your use case can
survive — a like counter can drift, a bank balance cannot.