Study Notes
Notefoundation

Concurrency in Online Services

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.

35 min read·verified Content review 2026·reviewed 2026-08-30

After this you should be able to

  • Explain the core idea in Concurrency in Online Services
  • Apply Concurrency in Online Services in a representative design
Distributed Systems · Chapter 01

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 void buyBook() { // 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.

buyers load bal. Server A · JVM🔒 own lock Server B · JVM🔒 own lock 1 DB both write at once ✕
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.

AnomalyWhat happens (bookstore example)
Dirty readT1 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 updateT1 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 readT1 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 readT1 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:

LevelDirty readLost updateNon-repeatablePhantom
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.

record snapshot snapshot T1 · own copywrites locally T2 · own copywrites locally merge / commit T1 commits ✓ T2 conflict → abort, 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?

OptimisticPessimistic
AssumesFew/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 bySnapshot isolation.Locking (e.g. serializable via read/write locks).
Trade-offBetter performance; conflicts cause aborts/retries at merge.Guaranteed consistency; performance hit from locking.
Pick whenConflicts 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.
Four isolation levelsSerializable (all prevented) → Repeatable Reads (phantoms possible) → Read Committed (only dirty prevented) → Read Uncommitted (none).
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.

Rewritten in full from the Concurrency in Online Services section (Chapter 1) of Designing Modern Web-Scale Distributed Services — merging its three teaching lessons (Transaction Isolation Levels, Database Locks, and Concurrency Control Mechanisms). This section has no quiz and cites no external links. Both diagrams are original SVGs built from the lessons' descriptions, not reproductions of source images. [+] marks context added beyond the source. This is the first chapter of a new course; the visual system matches the earlier Web Application & Software Architecture 101 study pages for continuity.