Mutable shared state anti-pattern
Learn why shared mutable state causes race conditions, data corruption, and heisenbugs in concurrent code, and how immutability, actors, and proper synchronization prevent them.
TL;DR
- Shared mutable state is data that multiple threads can read and write simultaneously without coordination. It causes race conditions, lost updates, and torn reads.
- Race conditions are timing-dependent: they pass every unit test and explode at 3 AM under production load.
- Three fundamental strategies: eliminate sharing (copy the data), eliminate mutation (immutability), or coordinate access (locks, atomics, actors).
- Immutability is the default choice. Java records and unmodifiable collections make shared data safe with zero coordination overhead.
- When mutation is truly required, use AtomicInteger for simple counters, ReentrantLock for complex state, or the Actor model for high-contention systems.
The Problem
Your e-commerce platform tracks inventory counts in a shared map. Two threads handle concurrent purchase requests:
// β Shared mutable inventory without synchronization
public class InventoryService {
private final Map<String, Integer> stock = new HashMap<>();
public boolean purchase(String itemId, int quantity) {
int available = stock.getOrDefault(itemId, 0);
if (available >= quantity) {
stock.put(itemId, available - quantity); // not atomic
return true;
}
return false;
}
}
Two customers buy the last item at the same time. Both threads read available = 1, both pass the check, both write stock = 0. You just sold one item twice.
I've debugged this exact bug in a ticketing system. The team had 100% unit test coverage. Every test passed because tests run single-threaded. The bug only appeared during flash sales when concurrent requests spiked.
The read-check-write sequence is a compound operation. Without synchronization, the thread scheduler interleaves these steps in any order. One write silently overwrites the other.
Why It Happens
- Single-threaded thinking. Most developers write and test sequentially. The mental model is "line A runs, then line B." With threads, lines from different threads interleave unpredictably.
- Invisible interleaving. The JVM and OS scheduler decide when to swap threads. Your code looks sequential. The execution is not.
- Tests lie. Unit tests run single-threaded by default. The race condition never fires during
mvn test. It surfaces under concurrent load in staging or production. - "It's just a counter." Developers underestimate the complexity of concurrent mutation. Even
count++is three operations (read, increment, write) that can interleave.
The testing trap
Race conditions are heisenbugs: they disappear when you observe them. Adding logging or breakpoints changes timing enough to hide the race. If a bug only reproduces under load and never in the debugger, shared mutable state is the first suspect.
How to Detect It
| Signal | What to look for | Tool |
|---|---|---|
| Non-final mutable fields in concurrent services | HashMap, ArrayList, or plain fields without synchronization | Code review, grep -rn "new HashMap" |
| Read-then-write patterns | get() followed by put() on shared collections | Manual review for compound operations |
| Flickering test failures | Tests pass alone, fail when run in parallel | Run with -DforkCount=4 or --parallel |
| "Works on my machine" bugs | Bug only appears under concurrent load | Stress testing with JMH or Gatling |
Missing synchronized, Lock, or Atomic | Service handling concurrent requests with plain fields | Static analysis (SpotBugs, Error Prone) |
volatile without atomicity | Field is volatile but compound read-write is not atomic | Review uses of volatile for correctness |
The Fix
The progression from simple to complex: start with immutability. If mutation is required, use atomics for simple state or locks for compound operations. For high-contention systems, consider the Actor model.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.
Related Articles
Learn why overusing singletons creates hidden global state, makes code untestable, introduces threading hazards, and how dependency injection replaces them cleanly.
Learn why a class that knows everything and does everything is the hardest to test, extend, and maintain, and how to break it apart using Single Responsibility and Extract Class.