Concurrency
5 min read
Core idea
Concurrency is unavoidable; shared state is optional
Any non-trivial program today is implicitly concurrent — users click, sockets read, services respond, timers fire, all at the same time. The previous topic attacked coupling between things. This topic attacks coupling between moments. The thesis is that almost every concurrency bug you have ever seen — race conditions, lost updates, deadlocks, phantom values — is a symptom of two errors made at design time: assuming a sequence that the problem does not require (temporal coupling) and letting two threads of execution touch the same piece of mutable memory (shared state).
Design for parallelism by default
You cannot bolt concurrency onto a serial design. The right move is to spot opportunities for parallelism while you are still drawing boxes on the whiteboard, and to choose primitives — actors, message queues, blackboards — that make sequential bugs structurally impossible rather than merely improbable.
Why it matters
Concurrency bugs are the hardest bugs
Race conditions and deadlocks rarely reproduce on demand. They survive code review, slip past unit tests, and surface in production at 3am. Locks help, but locking is its own minefield — the more locks you add, the more deadlock paths you create. The only durable fix is to design the bug out of existence by removing the conditions it requires. Take away the shared mutable state and the race has nothing to race for.
The performance ceiling is parallel
Single-thread CPU clock speeds stopped climbing roughly 15 years ago; future performance lives in cores, not gigahertz. Code that cannot exploit multiple cores is code that cannot grow. The shape of the program — whether it is "do A, then B, then C" or "fire A, B, and C in parallel and reconcile" — decides whether new hardware helps it at all.
Key takeaways
The topics in this topic
Breaking Temporal Coupling
Tip: Analyze workflow to improve concurrency.
Temporal coupling is the silent half of coupling — it hides in the word "then." Method A must run before B; the report must finish before the next one starts; the screen must redraw before the click registers. Most of these "musts" are inherited from sequential thinking, not required by the problem. The remedy is to draw the workflow as an activity diagram: list every step, then ask of each pair "does X really need to finish before Y?" The steps with no real dependency are the seams where concurrency belongs.
A concrete example. A page-render path fetches user profile, recent orders, and recommendations, each from a separate service. Sequentially this costs 90 + 120 + 150 = 360ms. Drawing the activity diagram exposes that none of the three requests depend on each other — fired in parallel, the page renders in 150ms. The change is structural, not algorithmic.
Shared State Is Incorrect State
Tip: Shared state is incorrect state.
The topic opens this topic with a restaurant story: two servers look at the dessert case at the same moment, both see one slice of pie, both promise it to a customer. Whichever server reaches the case second has to apologise. The bug was not in the looking or the promising — it was in the gap between the read and the write, during which the world changed without telling them. Every race condition in software is a version of that gap.
The conventional fix is locking, but locks are notoriously easy to misuse: forget one and the race returns; overuse them and deadlocks emerge; nest them wrong and performance collapses. The Pragmatic prescription is to avoid the situation entirely by removing the shared mutable state — make data immutable, make access serial through a queue, or push the mutation into a single owning component. Shared state is not just memory; it includes files, database rows, and any external resource two components can touch at once.
Actors and Processes
Tip: Use actors for concurrency without shared state.
An actor is an independent virtual processor with private state and a mailbox. When a message lands in the mailbox and the actor is idle, it processes the message to completion — creating other actors, sending messages to actors it knows about, or updating its own state — and then waits for the next message. Messages are one-way; replies are just more messages addressed back to the sender. There is no shared memory, no synchronisation primitives, no locks. Concurrency falls out of the model for free, because there is nothing to share.
A practical example. An order-processing system models inventory, payment, shipping, and notification each as actors. An incoming order is a message; the order-processing actor sends inventory a reserve message, waits for the confirmation message, sends payment a charge message, and so on. Two orders arriving at once cannot corrupt each other — each actor handles one message to completion before starting the next — and the system scales horizontally by adding more actors of each role.
Blackboards
Tip: Use blackboards to coordinate workflow.
A blackboard is half data store, half intelligent message broker. Producers post facts to it — "user 42 just logged in," "file report.csv has finished uploading" — and any interested consumer reacts. The producer does not know who is listening; the consumer does not know who is publishing. The blackboard's job is to match facts to subscribers based on pattern, type, or rule.
The original blackboard architecture, born in 1970s AI research, never became mainstream in its pure form. But it lives on, with variations, in everything modern systems do for coordination: message queues, event meshes, tuple spaces, reactive event buses, and document-database change feeds. Use a blackboard when many independent producers and consumers need to collaborate without knowing each other — the kind of problem where adding a new consumer should not require changing any producer.
Mental model
Related lessons
Related concepts
- Temporal Couplinglinked concept
- Shared Statelinked concept
- Actor Modellinked concept
- Blackboard Architecturelinked concept