Clean Code Chapter 13: Concurrency

Principles

Concurrency is a design concern of its own. Mixing thread policy into ordinary business logic hides races and makes failures intermittent, the worst kind to debug.

Keep it separate and minimal

Correct single-threaded design first. Then introduce concurrency deliberately, with clear ownership of shared state. Prefer immutability and isolation over synchronized spaghetti.

Know your shared data

Every mutable value touched by more than one thread needs a story: locked critically and briefly, atomic, confined to one thread, or redesigned away.

Limit synchronized scope

Wide locks kill throughput and raise deadlock risk. Narrow critical sections; do not hold locks while doing IO or calling untrusted code.

Understand the execution model

Threads, pools, actors, event loops, each has different failure modes. Use the platform’s executors and concurrent collections instead of hand-rolling queues.

Myths worth dropping

Concurrency does not automatically make things faster. It does not remove the need for design. And “it worked on my machine under light load” is not evidence the shared state is safe.

Defense in depth

Limit the scope of synchronized data. Use copies when crossing boundaries so callers cannot mutate your internals. Prefer known libraries (executors, concurrent collections) over home-grown locking schemes. Keep concurrent code out of the domain core when you can.

Test under stress

Races rarely show up in happy-path unit tests. Repeat, load, and shake schedules. Treat “it passed once” as weak evidence.

In code

Examples below are in Java.

Shared mutable state:

public class Counter {
  private int value;

  public void increment() { // race
    value++;
  }
}

Safer approaches:

public final class Counter {
  private final AtomicInteger value = new AtomicInteger();

  public void increment() {
    value.incrementAndGet();
  }

  public int get() {
    return value.get();
  }
}

Separate concurrency policy from work:

public final class OrderProcessor {
  public void process(Order order) {
    // pure domain, no threads here
  }
}

ExecutorService pool = Executors.newFixedThreadPool(8);
pool.submit(() -> processor.process(order));

What stuck

Get the single-threaded design right first. Keep shared mutable state rare and obvious. Stress-test; intermittent passes do not count.