System designby Learnastra

Concept lesson · Foundations

Transaction isolation

By Anup Rai

Start here

Definition

Transaction isolation defines how concurrent database transactions may observe and affect one another. A transaction groups operations into a unit that commits or aborts; its isolation level determines which interfering executions are permitted.

Why it matters: Two individually valid requests can make incompatible decisions from the same old data, even inside one database server.

The visual modelSnapshot isolation and the write-skew anomaly

Both transactions see another dispatcher on duty and update different rows. Snapshot isolation can permit both commits.

Snapshot isolation and the write-skew anomalyBoth transactions see another dispatcher on duty and update different rows. Snapshot isolation can permit both commits. Initially dispatchers D1 and D2 are both on duty. The invariant requires at least one dispatcher to remain on duty. Transaction T1 reads D2 on duty and turns D1 off. Concurrent transaction T2 reads D1 on duty and turns D2 off. They update different rows, so ordinary write-write conflict checks need not stop both. Serializable execution or an exclusively locked common guard row prevents the forbidden combined outcome.Invariant: at least one dispatcher stays on dutyInitial state: D1 ON, D2 ONTransaction T1reads: D2 is ONwrites: D1 OFFTransaction T2reads: D1 is ONwrites: D2 OFFBoth commit: D1 OFF, D2 OFFFix: serialize, or exclusively lock the roster guard.Retry the aborted transaction against the new state.Different rows can still participate in one cross-row invariant.
Read the diagram step by step
  1. Initially dispatchers D1 and D2 are both on duty. The invariant requires at least one dispatcher to remain on duty.
  2. Transaction T1 reads D2 on duty and turns D1 off. Concurrent transaction T2 reads D1 on duty and turns D2 off.
  3. They update different rows, so ordinary write-write conflict checks need not stop both.
  4. Serializable execution or an exclusively locked common guard row prevents the forbidden combined outcome.

Worked example

Rows D1 and D2 are on duty. Concurrent T1 and T2 each read count 2, then disable D1 and D2 respectively. Both committing leaves count 0, violating count >= 1.

Key takeaways

  • Atomicity groups changes; isolation governs concurrent decisions.
  • A stable snapshot can still permit write skew across different rows.
  • Serializable execution or a correctly acquired shared guard can protect the roster rule.

You will learn to

  • Explain isolation anomalies with specific concurrent reads and writes.
  • Choose an atomic update, shared lock, or serializable transaction for a stated invariant.
  • Design safe full-transaction retries without duplicating external effects.

Practice in this chapter

8 interview questions with model answers and follow-ups.

Go to interview practice

Useful foundations: Databases, data models, and ACID transactions · Consistency models

Workload and timing examples are interview assumptions.

01Transaction isolation: definition and example

Transaction isolation defines how concurrent database transactions may observe and affect one another. A transaction groups operations into one unit: committing accepts its changes, while aborting discards them. Atomicity provides that all-or-nothing grouping; isolation determines which interfering executions are allowed. Atomicity alone does not keep an earlier decision valid while someone else changes the database.

A cross-row constraint exposes the difference between atomicity and isolation. Roster R7 contains rows D1 and D2, both on_duty = true, with the invariant count(on_duty) >= 1. Transaction T1 attempts to disable D1; T2 attempts to disable D2. Each procedure reads the count and proceeds only when it exceeds one. The following hypothetical interleavings test whether the isolation mechanism preserves the constraint.

One server does not solve this problem automatically. A database on one machine still runs concurrent transactions; two browser requests can read before either has written. “We use SQL” and “we put it in a transaction” are incomplete answers until we know the isolation level, statements, constraints, and retry behavior. Begin with the invariant—the condition that every committed state must preserve—then examine whether concurrent executions can break it.

Worked example diagramWrite skew: two individually reasonable updates to different rows jointly violate the roster’s cross-row rule.
Transaction isolation: architecture diagram1. D1=on; D2=on to 2. T1 reads count 2: T1 snapshot; 1. D1=on; D2=on to 3. T2 reads count 2: T2 snapshot; 2. T1 reads count 2 to 4. T1 writes D1=off: different row; 3. T2 reads count 2 to 5. T2 writes D2=off: different row; 4. T1 writes D1=off to 6. Count 0: invalid: both commit; 5. T2 writes D2=off to 6. Count 0: invalid: both commit1 → 2: T1 snapshot1 → 3: T2 snapshot2 → 4: different row3 → 5: different row4 → 6: both commit5 → 6: both commit01D1=on; D2=on02T1 reads count 203T2 reads count 204T1 writes D1=off05T2 writes D2=off06Count 0: invalid
  1. 1 → 2T1 snapshotD1=on; D2=on → T1 reads count 2
  2. 1 → 3T2 snapshotD1=on; D2=on → T2 reads count 2
  3. 2 → 4different rowT1 reads count 2 → T1 writes D1=off
  4. 3 → 5different rowT2 reads count 2 → T2 writes D2=off
  5. 4 → 6both commitT1 writes D1=off → Count 0: invalid
  6. 5 → 6both commitT2 writes D2=off → Count 0: invalid

02Read anomalies: dirty, nonrepeatable, and phantom reads

A read anomaly is a named observation that a stronger isolation level rules out. The names below distinguish seeing uncommitted data, seeing a previously read row change, and seeing the set of matching rows change. These cases let us compare what concurrent transactions may observe before considering the roster’s write rule.

A dirty read observes another transaction’s uncommitted work. T2 tentatively changes D2 to off-duty; T1 reads that value; T2 then aborts. T1 has used a state that never committed. Read Committed prevents that anomaly, but does not necessarily give every statement in a transaction the same snapshot.

Anomaly Concurrent operations What changes
Dirty read T1 reads D2=off written by uncommitted T2; T2 aborts Uncommitted value leaks
Nonrepeatable read T1 reads D2=on; T2 commits D2=off; T1 reads D2=off Same row differs
Phantom read T1 counts 2 on-duty rows; T3 inserts D3=on; T1 counts 3 Predicate result gains a row

A predicate is the condition selecting a set, such as roster_id = R7 AND on_duty = true. Locking only the rows currently returned does not generally protect every future row matching that condition. Database engines differ in their predicate or range protection. Recognize the exact set-level race before assuming that a row lock covers it.

The familiar four SQL isolation names are minimum contracts, not identical implementations:

SQL isolation level Dirty reads Nonrepeatable reads Phantoms Serialization anomalies
Read Uncommitted Permitted by the standard Possible Possible Possible
Read Committed Prevented Possible Possible Possible
Repeatable Read Prevented Prevented Permitted by the standard Possible
Serializable Prevented Prevented Prevented Prevented

PostgreSQL maps Read Uncommitted to Read Committed and its Repeatable Read also prevents phantoms. That stronger snapshot guarantee still permits the write-skew schedule below. Treat a database's tested behavior and documentation as the implementation contract.

03MVCC, statement snapshots, and transaction snapshots

A snapshot determines which row versions a query or transaction can see. It provides a defined view of the data while other transactions may be changing it. Multi-version concurrency control, abbreviated MVCC, retains row versions so readers can use such a view while other transactions make progress. Versions cost storage and cleanup work; long-running readers can delay reclamation.

Concept in focusTwo readers can see different versions of one row

The arrows show which committed version each snapshot can see. Snapshot timing depends on the database and isolation level.

Two readers can see different versions of one rowThe arrows show which committed version each snapshot can see. Snapshot timing depends on the database and isolation level. Follow earlier and later snapshots to their visible versions. A row changes from x = 8 to committed x = 9. An earlier snapshot still reads 8; a later snapshot can read 9. MVCC alone does not guarantee serializability.One row, two committed versionsx = 8x = 9update commitsEarlier snapshotLater snapshotThe earlier snapshot can still read 8 after version 9 commits.Keep old versions while a required snapshot can still see them.

Remember: A new physical version does not erase an older reader’s snapshot.

Read the diagram
  1. Follow earlier and later snapshots to their visible versions.
  2. A row changes from x = 8 to committed x = 9.
  3. An earlier snapshot still reads 8; a later snapshot can read 9.
  4. MVCC alone does not guarantee serializability.
Try from memoryDoes reading 8 prove that version 9 failed to commit?

No. Version 9 can be committed but outside the reader’s earlier snapshot.

In PostgreSQL’s Read Committed mode, an ordinary query uses a fresh statement snapshot. Two queries in T1 can therefore observe different committed roster states. PostgreSQL Repeatable Read uses a stable transaction snapshot and also prevents the phantom-read phenomenon shown above, although the SQL standard’s minimum Repeatable Read guarantees are weaker. Always name the implementation when discussing that detail.

04Write skew versus lost updates

Write skew occurs when transactions read overlapping state but update different items, allowing their combined effects to violate a constraint. In the roster example, T1 and T2 both evaluate the same initial count of two and update different rows.

Concept in focusWrite skew: disjoint writes can break one rule

T1 and T2 update different dispatcher rows but share the rule that someone must remain on duty. Snapshot isolation alone may permit this write skew.

Write skew: disjoint writes can break one ruleT1 and T2 update different dispatcher rows but share the rule that someone must remain on duty. Snapshot isolation alone may permit this write skew. T1 to Database: Snapshot read: D1 on duty, D2 on duty. T2 to Database: Same initial snapshot: D1 on duty, D2 on duty. T1 to Database: Write D1 off, assuming D2 remains on. T2 to Database: Write D2 off, assuming D1 remains on. Database to Database: Both can commit under snapshot isolation: no dispatcher remains on duty.T1DatabaseT2Snapshot read: D1 on duty, D2 on duty.Same initial snapshot: D1 on duty, D2 on duty.Write D1 off, assuming D2 remains on.Write D2 off, assuming D1 remains on.Both can commit under snapshot isolation: no dispatcher remainson duty.

Remember: Different rows can still share one invariant.

Read the diagram
  1. T1 to Database: Snapshot read: D1 on duty, D2 on duty.
  2. T2 to Database: Same initial snapshot: D1 on duty, D2 on duty.
  3. T1 to Database: Write D1 off, assuming D2 remains on.
  4. T2 to Database: Write D2 off, assuming D1 remains on.
  5. Database to Database: Both can commit under snapshot isolation: no dispatcher remains on duty.
Step T1 T2
1 Reads D1=on, D2=on; count=2 Reads D1=on, D2=on; count=2
2 Decides leaving is allowed Decides leaving is allowed
3 Writes D1=off Writes D2=off
4 Commits Commits
Result No dispatcher remains Invariant broken

This outcome has no valid serial explanation. If T1 completed first, T2 would read only D2 as on-duty and refuse to disable it. Reversing the order gives the symmetric result. Because the writes affect different rows, detecting only same-row write conflicts is insufficient.

Compare a lost update within this same roster service. Two requests read R7’s revision as 8 and both later assign 9; one logical increment disappears. An atomic revision = revision + 1 or a conditional version check addresses that counter race. Fixing it does not automatically fix the cross-row on-duty rule.

05Serializable isolation, conditional updates, and guard locks

Serializable isolation promises that committed transactions have the same effect as some serial execution. It need not literally execute them one at a time. Implementations may block conflicts, detect them and abort work, or combine techniques. This is a transaction-order promise; strict serializability additionally respects real-time order of non-overlapping transactions.

Approach Good fit Price for this roster
Conditional single-row update Invariant truly fits one authoritative row Data model may need an aggregate
Exclusive lock on a common guard row Small, known conflict domain R7 changes wait behind each other
Serializable transaction Richer reads and evolving invariants Detect conflicts and retry whole work

I choose a guard row for this small, frequently reviewed roster workflow. It makes the serialization point explicit: every transaction changing R7 must acquire the lock on the same row. I would revisit that choice if the operation grows into many independent rosters or complex predicates.

Isolation levels describe allowed outcomes; concurrency-control techniques determine how the database prevents disallowed ones. An optimistic approach performs work and validates that relevant state has not changed before accepting the update. A pessimistic approach acquires protection first. Compare-and-set is one atomic conditional-update mechanism that can support such validation.

Name the concurrency technique as well as its implementation:

Technique Mechanism Choose when Cost or trap
Optimistic concurrency control Read a version; accept the write only if the version still matches Conflicts are uncommon and retries are cheap A failed condition requires reread/recompute; every relevant writer must advance the version
Compare-and-set Atomically change a value only if it equals the expected value or version One atomic object contains the invariant Comparing a reused value can miss an intervening change; an ever-increasing version avoids this ABA problem, where a value changes from A to B and back to A between the original read and the comparison
Pessimistic locking Acquire a lock before the protected read and hold it through commit Contention is expected or the read/modify sequence must be serialized Waits, deadlocks, and long transactions; use a consistent lock order and bounded work

For example, UPDATE item SET value = :new, version = version + 1 WHERE id = :id AND version = :seen succeeds only when the affected-row count is one. A zero-row result means conflict or absence, not permission to overwrite anyway. This protects that row’s change; it does not automatically protect a rule spanning other rows.

06Serialization retries and external side effects

A transaction may abort and rerun, so do not send “you are off duty” inside it. Save a pending notification in an outbox in the same transaction as the roster change. Send it afterward with duplicate protection. If the commit reply is lost, reuse an operation ID so a retry can find the saved result.

A concrete PostgreSQL Read Committed implementation uses separate statements inside one transaction:

BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT roster_id FROM roster_guard
WHERE roster_id = 'R7' FOR UPDATE;
-- Require exactly one existing guard row; otherwise abort.
SELECT count(*) FROM dispatcher
WHERE roster_id = 'R7' AND on_duty;
-- If count > 1 and D1 is currently on duty in R7:
UPDATE dispatcher SET on_duty = false
WHERE roster_id = 'R7' AND dispatcher_id = 'D1' AND on_duty;
-- Check the affected-row count; record outcome and outbox intent here.
COMMIT;

The application branches on the count; the comments are required control flow, not executable enforcement. Keep the guard held until commit. A missing guard row acquires no row lock, so create it as part of roster creation and reject missing guards. Do not combine lock acquisition and the protected count into one statement whose snapshot may predate a lock wait. This protocol also requires deletes and transfers to acquire the same guard before their decisions.

07Interview explanation: invariant, mechanism, and contention

A strong interview explanation begins: “The invariant is at least one on-duty dispatcher per roster. Two concurrent leave transactions can each read two and update separate rows, so atomicity and snapshot isolation alone are insufficient. I will have each transaction lock the R7 guard row before reading the current count, and require every membership change to obey that protocol.”

Then describe operational cost. A slow transaction holding the guard blocks other R7 changes, so no user interaction or remote API call belongs inside it. Transactions acquiring several roster guards should use a consistent order to reduce deadlocks; the system must still handle deadlock aborts. Measure lock wait time, transaction duration, abort rate, and retries per completed request.

Test the two requests together: run T1 and T2 concurrently and check that exactly one doctor goes off duty. Then drop the response after commit and retry with the same operation ID; the retry should find the saved result. Check waiting and retry limits too.

Practise the interview questions

Say your answer aloud before opening the model answer. Then answer the follow-up and compare the reasoning.

Foundation · Question 1

What is transaction isolation, and why is it different from atomicity?

Reveal a model answer

Atomicity makes a transaction’s changes commit together or abort together. Isolation controls how concurrent transactions observe and interfere with one another. In the roster example, two atomic leave requests can both read 2 and update different rows, leaving 0 on duty under snapshot isolation. The database needs a concurrency rule that protects the shared business condition.

What the answer must demonstrate: Separate all-or-nothing changes from safe concurrent decisions.

Foundation · Question 2

Explain nonrepeatable and phantom reads without jargon.

Reveal a model answer

“A nonrepeatable read occurs when transaction T1 reads row D2 twice and observes different committed values because another transaction changed it. A phantom occurs when T1 repeats a predicate query, such as all on-duty rows, and sees a new or missing matching row. The first concerns an existing row’s value; the second concerns membership in a result set.”

What the answer must demonstrate: Use one row versus a matching set.

Applied · Question 3

Does Repeatable Read prevent phantoms?

Reveal a model answer

“I would ask which database. The SQL standard’s minimum guarantees allow that phenomenon, while PostgreSQL Repeatable Read uses a stable snapshot and prevents it. Neither statement means PostgreSQL Repeatable Read prevents our write-skew example.”

What the answer must demonstrate: Do not generalize product behavior from the level name.

Applied · Question 4

Two transactions read revision 8 and both assign 9. How do you prevent this lost increment?

Reveal a model answer

“I use an atomic increment or a compare-and-update against the expected revision, checking whether it succeeded. Reading 8 in application code and later assigning 9 in both requests loses one increment.”

What the answer must demonstrate: A local race fix must cover the business decision to enforce it.

Applied · Question 5

To protect count(on_duty) >= 1 using a shared guard row, when must the guard be locked relative to reading the count?

Reveal a model answer

“Before reading the state used to decide whether someone may leave. I use a transaction pattern whose post-lock query observes the previous holder’s committed result; with Read Committed, a subsequent query gets a fresh statement snapshot.”

What the answer must demonstrate: Lock timing and snapshot timing must agree.

Follow-up · Question 6

T2 receives a serialization failure. What does the application do?

Reveal a model answer

“Abort the failed attempt and retry the complete transaction: reads, validation, and writes. If another transaction reduced the on-duty count to one, the new execution must reject the off-duty transition. Retrying only the final write reuses an invalid decision.”

What the answer must demonstrate: Retries must recompute the decision.

Follow-up · Question 7

How do you emit an off-duty notification only for a committed transition when its transaction may abort and retry?

Reveal a model answer

“I record the notification intent atomically with the successful roster transaction. A separate worker sends it using a stable event identifier. The retried transaction body must not perform irreversible external work.”

What the answer must demonstrate: Explain which database changes commit together and which later message delivery still needs deduplication.

Applied · Question 8

What operational costs should you measure for a guard row that serializes all changes to one roster?

Reveal a model answer

“I measure wait time, transaction length and contention by roster. A long-held guard is a latency bottleneck even if CPU looks idle. I keep the protected work short and test simultaneous leave, deletion and transfer operations.”

What the answer must demonstrate: More concurrency can worsen a serialized bottleneck.

Blank-page exercise · 15 minutes

Build the answer yourself

Protect count(on_duty) >= 1 for roster R7. Show T1 and T2 reading count 2 and disabling different rows, then compare a shared guard and serializable execution with full-transaction retries.

  • State the cross-row invariant and all operations that can affect it.
  • Draw reads, writes, and commits for the bad execution.
  • Explain exactly where conflicting operations are detected or serialized.
  • Keep notifications outside retried transaction bodies using an outbox.

Check that each component and design decision follows from your requirements and workload.

Recall the key ideas

Answer from memory before opening each card. Explain why the choice works and what it costs. Revisit missed cards tomorrow.

Transaction isolationWhy can two valid snapshot transactions create an invalid roster?Recall first, then reveal

They read the same old set and write different rows, so their combined effect may have no valid serial explanation.

Stable picture ≠ safe decision

Return to lesson
Transaction isolationWhat must be retried after a serialization failure?Recall first, then reveal

The entire transaction, including the reads and decision that produced its writes.

Reread, rethink, rewrite

Return to lesson
Transaction isolationWhat does a guard row protect?Recall first, then reveal

Only operations that acquire it under the agreed protocol before making protected decisions.

All doors use the same lock

Return to lesson
Transaction isolationAre serializable and linearizable identical?Recall first, then reveal

No. Serializability orders transactions by equivalent effects; strict serializability also enforces real-time order.

Serial is order; strict adds time

Return to lesson

Final revision

Summary and interview notes

Isolation governs concurrent decisions; atomicity only makes one transaction’s changes succeed or fail together. Protect the actual invariant with an appropriate database constraint, a correctly acquired guard, or serializable execution with whole-transaction retries.

Remember these points

  • A stable snapshot can permit write skew when transactions read shared state and update different rows.
  • Lock the guard before the decision and use a read view that includes the previous holder’s committed work.
  • Every operation that can break the invariant must obey the same concurrency protocol.
  • After a serialization failure or deadlock, retry the whole transaction. Save pending external actions so they can run safely after commit.

Interview tips

  • Write the invariant as a predicate and demonstrate an interleaving that violates it.
  • Name the database and isolation level before claiming which anomalies are prevented.
  • Explain lock wait, deadlock handling and the retry limit alongside the successful transaction.

Important qualifications

  • PostgreSQL Repeatable Read prevents phantoms but is not serializable; its documented behavior exceeds the SQL minimum for that level.
  • SELECT FOR UPDATE cannot lock an absent guard row; the guard must exist and remain protected for the transaction.

Technical references

Practice marks stay in this browser.