Foundations

Concurrency & Shared State

Intermediate

Concurrency bugs are the worst kind: rare, unpredictable, invisible in testing, and severe in production. They appear when several things touch shared state at once without coordination. The most reliable defence is not clever locking. It is not sharing mutable state in the first place.

A web service is concurrent by nature: many requests run at once, across threads and instances. Most of the time that is fine, because each request works on its own data. Trouble starts wherever they share something mutable — a static field, a cached object, a row two requests both update, a counter. There the order of operations becomes unpredictable, and 'works on my machine' tells you nothing.

There are two failure modes to design against. The first is a race: interleaved operations produce a state neither one intended. The second is a lost update: two read-modify-write cycles run, and one silently overwrites the other. In a financial system these are not glitches. They are double-spends, miscounted balances, and duplicated effects. Prefer designs that avoid shared mutable state. Where state must be shared, coordinate access explicitly.

Avoid shared mutable state

Coordinate when you must share

Lost update var acct = Load(id);
acct.Balance -= amount; // two requests both read the old balance
Save(acct); // the second Save overwrites the first

Under concurrent requests, one debit silently disappears: both read the same starting balance, and the later write wins. Money vanishes with no error.

Atomic, guarded update var rows = conn.Execute(@"UPDATE Accounts SET Balance = Balance - @amt, Version = Version + 1
WHERE Id=@id AND Balance >= @amt AND Version=@expected", p);
if (rows == 0) throw new ConcurrencyConflict();

The update is a single atomic statement, guarded by the expected version and a sufficient-funds check. A concurrent change is detected, not silently lost.

Self-review checklist

Why it matters: Concurrency defects pass every test and then corrupt data under real load, exactly when the stakes are highest. In a payments or AML system that means lost money, double effects, and balances that do not reconcile. Designing out shared mutable state, and coordinating carefully where it is unavoidable, is the only reliable way to be correct under concurrency.