Interview Handbook by Sayed Elmahdy العربية

Topic 12 · Data & databases

Data consistency & transactions

Isolation anomalies, MVCC, locking, replication, consistency models, distributed transactions.

17 questionsJunior · 4Mid · 7Senior · 610 quiz

Concepts

What you will learn: Imagine the last seat being booked twice or two edits overwriting one another: every operation can succeed technically while a business rule becomes false. We will study anomalies, , locks, and , then distinguish a local from consistency across replicas or services.

Before you start: A transaction is a group of changes committed or rolled back together; an is a rule that must remain true, such as stock never falling below zero. Review transactions in topic 10. If these terms are new, start with the first card before the isolation-level table.

Select a term with a dotted underline for its meaning, then continue reading from the same place.

#A transaction alone is not enough

If two customers buy the last item, persistence may have succeeded perfectly: both committed decisions based on stale reads. Atomicity makes your steps succeed or fail together. controls their interaction with other transactions. Start with an such as Stock >= 0, then choose a mechanism that actually enforces it.

A transaction is a sealed envelope of instructions executed together. Two sealed envelopes can still reserve the same seat.
BEGIN TRANSACTION;                              -- (1)
UPDATE Products SET Stock = Stock - 1
WHERE Id = @id AND Stock >= 1;                   -- (2)
-- Application: require exactly one affected row before inserting the order.
-- Otherwise roll back and report unavailable stock.
INSERT INTO Orders (Id, ProductId) VALUES (@orderId, @id); -- (3)
COMMIT;                                         -- (4)
🧾 Code walkthrough
  1. (1) Start the shared persistence unit.
  2. (2) Check and decrement in one statement; the caller must inspect the affected-row count.
  3. (3) Insert the order only after successful reservation.
  4. (4) Commit both changes; roll back failures before commit.

This is a control-flow fragment, not an unchecked batch. Use parameters and a constraint as additional protection. Avoid payment calls inside long transactions: the database cannot roll back an external charge.

#Read anomalies: follow the timeline

A worked example
  1. The committed balance is 100. A writes 80 without committing; B reads 80; A rolls back. B used an uncommitted value: a dirty read.
  2. For a non-repeatable read, B first reads 100, A commits 80, then B rereads the row and sees 80. Both values were committed when read, but the reads differ.
  3. A phantom concerns rows matching a predicate: repeating the query may include a newly inserted matching row. Draw the timeline and distinguish a row's value from membership of a result set.

An anomaly describes an execution history, not an exception type. A and B are separate ; arrows show time.

AnomalyTimelineFailure
Dirty readA: write price=80 → B: read 80 → A: rollbackB observed a value that never committed.
Non-repeatable readA: read price=100 → B: commit price=80 → A: read 80The same row changed between reads.
Phantom readA: count pending=4 → B: insert pending, commit → A: count=5The same predicate returned a different set.
-- SQL Server, session A
BEGIN TRANSACTION;
UPDATE Products SET Price = 80 WHERE Id = 1;   -- (1) not committed
-- Pause here; execute session B, then:
ROLLBACK;

-- SQL Server, session B
SELECT Price FROM Products WITH (NOLOCK) WHERE Id = 1; -- (2)
🧾 Code walkthrough
  1. (1) This tentative change can still disappear.
  2. (2) NOLOCK permits uncommitted reads and can miss or duplicate rows. It is not a general performance fix and still takes locks.

This intentionally demonstrates unsafe reading. Start with indexes, short transactions and an appropriate level instead of silently weakening report correctness.

#Lost updates and write skew

A lost update occurs when two writers calculate from the same old value and the later write overwrites the earlier result. With write skew, they change different rows after checking a shared condition; there is no same-row conflict for a rowversion to detect.

StepLost update: Stock = 10Write skew: two doctors on call
1A and B read 10Both observe two doctors available
2A writes 9 and commitsA goes off call and commits
3B writes 9 and commitsB goes off call and commits
ResultTwo orders, only one decrementNo doctor remains, violating the minimum of one
-- Each doctor runs this in a separate snapshot transaction.
SELECT COUNT(*) FROM Doctors WHERE OnCall = 1;       -- (1) both see 2
-- Application proceeds only if count > 1.
UPDATE Doctors SET OnCall = 0 WHERE Id = @myId;       -- (2) different rows
🧾 Code walkthrough
  1. (1) A stable snapshot does not make decisions safe from other decisions.
  2. (2) Different rows allow both snapshot to succeed.

Use Serializable with transaction retries, or lock a shared roster row before checking. Locking only each doctor's own row is insufficient. Protect the required rather than raising globally without measurement.

#Isolation levels: standard versus implementation

This table shows -standard guarantees for the three read anomalies. “Possible” does not mean inevitable. Lost updates and write skew need additional analysis of the engine and write operations.

LevelDirtyNon-repeatablePhantom
Read UncommittedPossiblePossiblePossible
Read CommittedPreventedPossiblePossible
Repeatable ReadPreventedPreventedPossible
SerializablePreventedPreventedPrevented

SQL Server: Read Committed is the default; traditional SQL Server defaults to off. Azure SQL Database enables RCSI by default. Locking Repeatable Read protects existing rows read, not all predicate ranges. PostgreSQL: the default is Read Committed; Read Uncommitted behaves identically. Repeatable Read uses snapshot and also prevents phantoms, but can allow write skew. Serializable detects dangerous dependencies and may abort a .

-- PostgreSQL: run business checks and writes within this transaction.
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;  -- (1)
SELECT COUNT(*) FROM Doctors WHERE OnCall = TRUE;
-- Application checks the count, then updates its doctor.
COMMIT;                                         -- (2)
🧾 Code walkthrough
  1. (1) Request an outcome equivalent to a serial ordering of successful transactions.
  2. (2) Success is not guaranteed: retry the whole unit with fresh reads and bounded attempts after failure.
Serializable does not mean one execution thread or freedom from retries. Local transaction isolation also does not guarantee linearizability across a distributed system.

Engine details: PostgreSQL isolation and SQL Server locking and versioning.

#Locks and pessimistic concurrency

When contention is expected, reserve the right to change data before making a decision. In Server, S read locks are mutually compatible; X write locks conflict with S and X on the same resource. An U update lock is compatible with S but not another U, reducing some read-to-write conversion , not every deadlock.

Row and page locks trade scope against tracking cost. SQL Server may escalate locks to a table or partition depending on configuration and pressure; row locks do not necessarily become page locks first. PostgreSQL has different row and table locking and does not use the same escalation policy.

-- PostgreSQL: same connection and transaction for read and write.
BEGIN;
SELECT Stock FROM Products WHERE Id = 7 FOR UPDATE;       -- (1)
-- Application checks available stock while holding the lock.
UPDATE Products SET Stock = Stock - 1 WHERE Id = 7;
COMMIT;

-- SQL Server equivalent read-intent fragment inside an explicit transaction:
SELECT Stock FROM Products WITH (UPDLOCK, HOLDLOCK)
WHERE Id = @id;                                          -- (2)
🧾 Code walkthrough
  1. (1) Lock the existing row until end; competing modifications wait. This does not automatically lock a missing row or an entire predicate range.
  2. (2) UPDLOCK requests update locks; HOLDLOCK applies Serializable semantics to this table reference, including relevant range protection. Indexes and query shape affect lock scope.

Never wait for user input or network calls while holding locks. A conditional UPDATE is simpler for a stock decrement. ROWLOCK does not guarantee that escalation cannot occur.

#MVCC, RCSI and SNAPSHOT

A worked example
  1. The committed price is 100. Another operation changes it to 120 while a reader works; retained row versions let the reader observe a version allowed by its level.
  2. Under Server , one statement can read 100 and a later statement read 120 after the update commits, because statements have separate snapshot boundaries.
  3. With SNAPSHOT in one , the snapshot is established at first data access and reused, alongside the transaction's own changes. Versions consume storage and do not eliminate write conflicts.

Readers blocking writers create queues. retains versions so a reader can often observe an allowed version instead of waiting for a writer. It neither eliminates locks nor solves write skew.

SQL Server approachSnapshot boundaryTrade-off
Locking Read CommittedCommitted reads protected by locksReader/writer blocking
Read Committed + RCSIStart of each statementA later statement may observe a newer commit
SNAPSHOTTransaction snapshot established at first data accessVersion storage and possible update conflicts
-- SQL Server: administrative setup, not per-request commands.
ALTER DATABASE Shop SET READ_COMMITTED_SNAPSHOT ON;       -- (1)
ALTER DATABASE Shop SET ALLOW_SNAPSHOT_ISOLATION ON;      -- (2)

-- On an application connection, after SNAPSHOT has been enabled:
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
SELECT Total FROM Orders WHERE Id = @id;                 -- (3)
-- Another transaction may commit here.
SELECT Total FROM Orders WHERE Id = @id;
COMMIT;
🧾 Code walkthrough
  1. (1) Change the Read Committed implementation; this administrative change needs connection planning.
  2. (2) Separately allow connections to select SNAPSHOT.
  3. (3) Both reads observe the same snapshot, plus the transaction's own changes.

Monitor version retention and long transactions. MVCC trades some waiting for storage and cleanup work. SQL Server SNAPSHOT and RCSI-backed Read Committed are distinct choices.

#Optimistic concurrency and atomic updates

Do not lock an order while a user edits a form. Return its version and accept the save only if that version is still current. Server rowversion is an automatically changing binary , not a date or time.

// using System.ComponentModel.DataAnnotations;
// using Microsoft.EntityFrameworkCore;
public sealed class Order
{
    public int Id { get; set; }
    public string Note { get; set; } = "";
    [Timestamp] public byte[] Version { get; set; } = [];  // (1)
}

var order = await db.Orders.SingleAsync(o => o.Id == id, ct);
// submittedVersion came from the original edit response.
db.Entry(order).Property(o => o.Version).OriginalValue = submittedVersion; // (2)
order.Note = submittedNote;
try { await db.SaveChangesAsync(ct); }
catch (DbUpdateConcurrencyException)
{
    return Results.Conflict("Order changed; reload and review your edit."); // (3)
}
🧾 Code walkthrough
  1. (1) The SQL Server provider maps this as a database-generated concurrency token.
  2. (2) Compare the user's original version, not one fetched after submission.
  3. (3) includes the version in the WHERE clause; zero affected rows signals conflict, prompting review rather than silent overwrite.

For a stock decrement, a preliminary read may be unnecessary:

ArgumentOutOfRangeException.ThrowIfNegativeOrZero(qty);   // (1)
int changed = await db.Products
    .Where(p => p.Id == productId && p.Stock >= qty)
    .ExecuteUpdateAsync(s => s.SetProperty(p => p.Stock, p => p.Stock - qty), ct); // (2)
if (changed == 0) return Results.Conflict("Stock unavailable"); // (3)
🧾 Code walkthrough
  1. (1) Negative quantities would increase stock.
  2. (2) SQL evaluates the condition and decrement together, avoiding application-side stale read-modify-write.
  3. (3) Inspect the result; this bypasses SaveChanges and does not automatically apply tracked concurrency tokens.

Wrap this and an order insert in a shared . Do not blindly retry edit conflicts or use a single-row token to protect a multi-row . EF Core concurrency.

#Deadlocks: diagnose and retry the whole unit

Blocking is a one-way wait. A is a cycle; the database chooses a victim and aborts its so others can proceed.

Time    Transaction A                 Transaction B
1       X lock Product 1              X lock Order 7
2       Request Order 7: waits        Request Product 1: waits   (1)
3       Victim rolls back             Survivor proceeds         (2)
🧾 Code walkthrough
  1. (1) Each holds the resource the other needs.
  2. (2) The victim must repeat reads and writes together, not just the last UPDATE.
  1. Capture the deadlock graph and identify resources and queries.
  2. Use a consistent resource order and shorten transactions.
  3. Review indexes and scans that widen lock scope.
  4. Retry recognized transient failures with bounded backoff and jitter.

With an execution strategy, place the entire explicit transaction inside the replayable . External side effects require or an ; retrying every exception can duplicate a payment.

#Replication and consistency modelsBonus

Optional but valuable: not asked in every interview, but knowing it deepens your understanding and sets you apart.

After an order update on the primary, a lagging replica may return the old state. Read-your-writes is a session guarantee: a client observes its earlier writes. Read from the primary or route using a position and wait for a replica to catch up. A fixed sticky interval is only a heuristic unless lag has a guaranteed bound.

ModelMeaningExample
LinearizableEach operation appears instantaneous between invocation and completion, respecting real timeA read begun after a completed reservation sees its effect or a later change
SequentialOne global order preserves each client's program order, not necessarily real time across clientsEveryone agrees on order, although another client may not yet see a completed write
CausalCauses precede effects; independent operations may have different ordersA response to an order edit cannot appear before its cause
EventualAfter writes stop and communication recovers, replicas converge using conflict resolutionA delayed view counter eventually catches up
Client A: write Order=Paid → primary acknowledges           (1)
Client A: read replica → Order=Pending                      (2)
Fix: read primary, or wait until replica position >= token  (3)
🧾 Code walkthrough
  1. (1) Primary acknowledgement does not necessarily acknowledge every replica.
  2. (2) Lag breaks read-your-writes.
  3. (3) The guarantee depends on routing or a , not an arbitrary .

: during a network partition, a system cannot guarantee both linearizability and successful completion of every request to nonfailed nodes. This is not an unrestricted “pick any two,” and consistency means something different. Choose guarantees per operation, accounting for communication and ; an approximate counter rarely needs global ordering.

#Distributed transactions and cache consistencyBonus

Optional but valuable: not asked in every interview, but knowing it deepens your understanding and sets you apart.

Saving an order and publishing an event to another system are independent writes. 2PC asks participants to prepare, then a coordinator decides commit or abort. It provides an atomic outcome for supported participants, but prepared participants may retain resources while awaiting a decision. Communication, operational cost and external-service support limit its usefulness.

A saga uses local with compensations: reserve stock, charge payment, confirm the order; release stock if payment is declined. Compensation is a new business operation that may fail or need intervention, not a time-reversing rollback or from intermediate states.

Producer transaction:
  UPDATE Order + INSERT Outbox(MessageId, OrderId, Version)   (1)
Dispatcher:
  publish message → crash before marking sent → publish again (2)
Consumer transaction:
  INSERT Inbox(ConsumerId, MessageId) with UNIQUE constraint  (3)
  apply local effect → commit → acknowledge broker           (4)
🧾 Code walkthrough
  1. (1) The closes the gap between saving data and recording publication intent in one database transaction.
  2. (2) Duplicates are expected; this is not end-to-end exactly-once delivery.
  3. (3) The inbox prevents repeated application by the same consumer. Handle the known duplicate-key case, not every database error.
  4. (4) Commit the marker and local effect together. External payment effects also require a provider-side key.

: update the database, then invalidate, using to limit stale retention. A reader can still fetch old data before the update and populate the cache after invalidation. Use version-aware population or coordination if a stronger guarantee is required; TTL alone is not strong consistency. An outbox helps eventually deliver invalidations but does not remove the delay window. Make correctness-sensitive decisions against the authoritative source.

ChoiceWhenCost
Local transactionData inside one boundarySimpler correctness
2PCSupported participants requiring atomic commitBlocking and coordination
Saga + outbox/inboxIndependent services and long workflows, compensation and monitoring

Interview questions

Check your reasoning

Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?

12.1Does putting two statements in a transaction prevent every race condition?Junior
Model answer

I separate atomicity from . A makes its changes commit or roll back together, but its reads can still become stale at common isolation levels. Two successful transactions can therefore violate a business rule. For stock, I prefer a conditional decrement and check the affected-row count. If several rows define the rule, I consider Serializable isolation or a shared lock boundary. I choose protection from the rather than assuming BEGIN TRANSACTION solves concurrency.

12.2What are dirty, non-repeatable and phantom reads?Junior
Model answer

I describe them with two . A dirty read observes another transaction's uncommitted change, which may later roll back. A non-repeatable read sees a different committed value when reading the same row again. A phantom means repeating a predicate query produces a different set of rows, such as an extra pending order. These distinctions matter because protecting existing rows is not necessarily enough to protect the range of rows matching a condition.

12.3Why is NOLOCK not a safe general performance fix?Junior
Model answer

I would not add NOLOCK just to make a report faster. It permits dirty reads and can return missing or duplicated rows, so the result may never have represented a committed database state. It also does not remove all locking, including locking. I investigate the execution plan, indexes and duration first. If reader-writer blocking is the issue, I assess row-versioned reads with their storage and semantic trade-offs rather than silently accepting incorrect results.

12.4What is a lost update?Junior
Model answer

A lost update happens when two callers read the same value, compute replacements separately, and the later write overwrites the earlier result. If stock starts at ten, two callers may both write nine after selling one item each. I avoid this with an atomic database expression, a concurrency- comparison, or a lock held across the decision. I also check business conditions such as sufficient stock, because an atomic decrement alone can still produce a negative quantity.

12.5How do SQL Server and PostgreSQL defaults differ?Mid
Model answer

I expect Read Committed in both engines, but I check how it is implemented and the actual database configuration. PostgreSQL uses a statement snapshot for Read Committed. Traditional Server defaults to locking reads unless READ_COMMITTED_SNAPSHOT is enabled; Azure SQL Database enables that option by default. PostgreSQL also treats Read Uncommitted as Read Committed. Those differences affect blocking and repeat reads, so I do not transfer assumptions between engines just because an -level name matches.

12.6What is the difference between RCSI and SNAPSHOT isolation?Mid
Model answer

In Server, changes Read Committed reads to use a committed snapshot for each statement. Two statements in one can therefore see different committed data. SNAPSHOT uses a transaction snapshot established at first data access, while still showing the transaction's own writes. It can raise update conflicts and requires separate enablement. I use the distinction when several reads must agree, and I still analyze multi-row because snapshot isolation is not equivalent to Serializable.

12.7How do you handle an EF Core rowversion conflict?Mid
Model answer

I configure a concurrency and preserve the version originally shown to the user. includes that original token in the update or delete predicate. If no row matches, SaveChanges raises DbUpdateConcurrencyException. I then apply an explicit policy: reload, show the conflict, or merge fields after reviewing database values. I do not blindly replace the original version and retry, because that can discard someone else's edit. Server rowversion is a binary token, not a timestamp value.

12.8When would you choose pessimistic rather than optimistic concurrency?Mid
Model answer

I prefer when conflicts are uncommon or an edit user interaction, since holding a database lock would be expensive. For a short decision under frequent contention, I may use SELECT FOR UPDATE in PostgreSQL or appropriate Server update locks. The protected read and write must share one and connection. I keep the lock scope short and plan for . For a single conditional counter change, one atomic UPDATE is often simpler than either approach.

12.9How would you prevent overselling the last product?Mid
Model answer

I validate that the requested quantity is positive and issue a conditional UPDATE that subtracts it only where the product has enough stock. I treat one affected row as a successful reservation and zero as failure. If creating the order must be atomic with reservation, both operations share a . This works across application instances because the database coordinates the write. I also add suitable constraints and a reservation identity if retries must not reserve the same order twice.

12.10How do blocking and deadlocks differ?Mid
Model answer

Blocking means one waits for another to release a conflicting resource. A is a cycle of dependencies, so waiting alone cannot resolve it. The database aborts a victim. I inspect the deadlock graph, use a consistent access order, shorten transactions and improve indexes where scans widen locking. I retry the victim's entire transaction with fresh reads and bounded backoff, making sure replay cannot repeat an unprotected external side effect such as charging a card.

12.11What does lock escalation mean?Mid
Model answer

In Server, escalation can replace many fine-grained locks with a broader table or partition lock, reducing lock-management overhead while potentially blocking unrelated work. The exact scope depends on configuration and workload. I investigate large scans, size and indexing rather than relying on ROWLOCK as a guarantee. I also avoid generalizing this mechanism to PostgreSQL, which has a different locking implementation and does not follow SQL Server's escalation policy.

12.12Why can snapshot isolation allow write skew?Senior
Model answer

Snapshot gives a consistent view but does not necessarily protect every predicate used in a decision. Two doctors can each observe that two people are on call, then update different rows to leave. Neither overwrites the other, yet nobody remains. A rowversion on each doctor does not fix that. I use Serializable with whole- retries or serialize the decision through a shared roster row locked before checking the count. The protected boundary must match the business .

12.13How would you guarantee read-your-writes with replicas?Senior
Model answer

I first define the scope of the guarantee, usually a user session. The straightforward solution is to route that session's relevant reads to the primary. Another option is a commit-position and a replica that waits until it has applied at least that position before serving the read. I would not call a fixed two-second sticky window a guarantee unless lag has a proven bound. I also make sure caches do not reintroduce older results.

12.14How do linearizable, sequential and causal consistency differ?Senior
Model answer

I think about the ordering each model promises. Linearizability makes each operation appear instantaneous and respects real-time completion across clients. Sequential consistency provides one order preserving each client's program order, but need not respect real time between clients. Causal consistency preserves cause-and-effect relationships while allowing independent operations to be observed in different orders. I choose the weakest guarantee that still supports the business rule, then check the , availability and session requirements instead of treating consistency as one switch.

12.15Why are sagas often chosen instead of two-phase commit?Senior
Model answer

Two-phase commit coordinates an atomic decision across supported participants, but prepared resources may remain held while the decision is unavailable. It also couples availability and requires infrastructure support many external lack. A saga instead commits local steps and performs compensating business actions when later steps fail. That supports independent services, but intermediate states become visible and compensation can fail. I prefer a local when the boundary permits it; a saga is a deliberate consistency trade-off, not a free replacement for .

12.16Do outbox and inbox patterns guarantee exactly-once processing?Senior
Model answer

I describe the guarantee precisely. An atomically records the business change and publication intent, but the dispatcher can publish twice after a crash. An inbox deduplicates messages for a consumer when its unique marker and local effect commit together. That gives effectively-once local effects under the stated assumptions, not universal exactly-once execution. If processing calls a payment provider, the inbox alone is insufficient; I need provider-side and reconciliation for ambiguous outcomes.

12.17Why can update-then-invalidate still leave a cache stale?Senior
Model answer

A reader may fetch the old database value before a writer commits. The writer then commits and invalidates the cache, after which the reader populates that cache with its old value. limits retention but does not prevent the race. Depending on the requirement, I use version-aware writes, coordination or authoritative reads for critical decisions. An improves eventual delivery of invalidations, but it still leaves a delay window and does not make the cache strongly consistent.

Test yourself

Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.

Score: 0 / 0

A reader observes a price that another transaction later rolls back. Which anomaly?

The value was read before commit and later disappeared through rollback.

SQL-standard Repeatable Read may still allow which read anomaly?

The standard permits phantoms; PostgreSQL provides a stronger guarantee here.

Two snapshot transactions turn off different doctors after both see two on call. What is this?

The is shared but the modified rows differ.

What snapshot boundary does SQL Server RCSI normally provide?

implements Read Committed with statement snapshots.

A conditional stock UPDATE affects zero rows. What should the application do?

Zero may mean missing product or insufficient stock; no reservation succeeded.

Which value should EF compare when saving an edited form?

The comparison detects changes since the form was loaded.

After a deadlock victim rollback, which unit should normally be retried?

Earlier decisions relied on reads from the aborted .

A read starts after another client's completed write. Which model preserves that real-time order?

Real-time ordering is the key additional guarantee.

An outbox dispatcher crashes after publishing but before marking sent. What is expected?

Publication may repeat, so the consumer must handle duplicates.

Does a cache TTL by itself guarantee reads always return the latest committed value?

limits retention; it does not coordinate read/write ordering.