Concurrency Interview Questions: The Failure Modes and Patterns Companies Actually Test

Quick Overview
Learn a repeatable concurrency interview method: identify shared state, state the invariant, then choose the smallest safe coordination mechanism. Covers races, deadlocks, blocking queues, idempotency, and distributed contention.
Concurrency questions are easier once you identify what can overlap and what must remain true while it does. A Software Engineer may be asked to protect an in-memory transfer, coordinate producers and consumers, or explain how two requests could reserve the same last item. These prompts use different tools, but they test the same habit: name the shared state, state the invariant, then choose the smallest mechanism that protects it.
This guide focuses on that reasoning path. It separates thread-safety problems from interval problems, explains the four failure modes worth naming, and works through the implementation patterns that recur in coding and system design rounds.
Read the prompt before reaching for a lock
“Concurrent” can describe three different situations. Only one automatically calls for synchronization primitives.
| Prompt shape | What overlaps | First tool to consider |
|---|---|---|
| Several threads update one object | Reads and writes to shared mutable state | Lock, atomic operation, immutability, or single ownership |
| Tasks use separate data but should run at the same time | CPU or I/O work | Thread pool, process pool, coroutine, or worker queue |
| Intervals overlap on a timeline | Trips, meetings, sessions | Sweep line or sorted endpoints |
Concurrency means several tasks can make progress during the same period. Parallelism means work actually executes at the same instant, usually on different cores. A web server can be highly concurrent on one core because tasks take turns while waiting for I/O. Sorting independent chunks on several cores is parallel because those chunks execute simultaneously.
The distinction matters because a lock only protects shared state. It does not make CPU work faster, and it is irrelevant to a “maximum overlapping meetings” problem.
Before writing code, say the invariant in one sentence. For a transfer it might be “the debit and credit happen atomically, and total money is conserved.” For a bounded queue it is “the size stays between zero and capacity.” That sentence tells you what must be protected and how large the critical section needs to be.
Four failure modes and their fixes
Interviewers rarely ask for definitions in isolation. They show a loop or an interleaving and ask what can go wrong. Use the names below only after you can point to the exact loss of progress or correctness.
| Failure mode | What happens | Typical cause | Common fix |
|---|---|---|---|
| Race condition | The result depends on an unsafe interleaving | Read-modify-write without one atomic boundary | Lock the whole invariant or use a suitable atomic primitive |
| Deadlock | Every participant waits forever | A cycle in lock acquisition | Global lock order, fewer locks, or a timeout plus recovery |
| Livelock | Threads keep reacting but no work completes | Symmetric retry-and-release behavior | Randomized backoff or deterministic ownership |
| Starvation | The system progresses while one participant never does | Unfair scheduling or reader preference | Fair queueing or an explicit handoff policy |
Consider two transfers running in opposite directions. If each thread locks its source account first, one can hold account A while waiting for B and the other can hold B while waiting for A.
A global order is the cleanest repair:
def transfer(accounts, source_id, target_id, cents):
if cents <= 0 or source_id == target_id:
raise ValueError("invalid transfer")
first_id, second_id = sorted((source_id, target_id))
with accounts[first_id].lock:
with accounts[second_id].lock:
source = accounts[source_id]
target = accounts[target_id]
if source.balance < cents:
raise ValueError("insufficient funds")
source.balance -= cents
target.balance += cents
The code protects one multi-object invariant. Replacing both locks with separate atomic balance updates would not be enough, because another thread could observe the debit before the credit.
Implementation patterns worth knowing
The strongest preparation is not memorizing a large concurrency library. It is knowing a few patterns well enough to explain why each line exists.
Bounded blocking queue
A producer waits while the queue is full; a consumer waits while it is empty. Both conditions share one lock because the queue state is one invariant.
import threading
from collections import deque
class BoundedQueue:
def __init__(self, capacity):
if capacity <= 0:
raise ValueError("capacity must be positive")
self.capacity = capacity
self.items = deque()
lock = threading.Lock()
self.not_empty = threading.Condition(lock)
self.not_full = threading.Condition(lock)
def put(self, item):
with self.not_full:
while len(self.items) == self.capacity:
self.not_full.wait()
self.items.append(item)
self.not_empty.notify()
def get(self):
with self.not_empty:
while not self.items:
self.not_empty.wait()
item = self.items.popleft()
self.not_full.notify()
return item
The while is essential. A wakeup does not guarantee the condition is still true when the thread reacquires the lock. Another consumer may have taken the item first, and some runtimes permit spurious wakeups. Both operations are O(1); storage is O(capacity).
Lock, atomic, immutable state, or one owner?
| Mechanism | Best fit | Important limit |
|---|---|---|
| Mutex | An invariant spans several reads and writes | Contention grows with the critical section; multiple locks require an order |
| Atomic operation | One counter, flag, or reference can change independently | Separate atomics do not protect a multi-field invariant |
| Immutable snapshot plus atomic swap | Readers need a consistent whole-object view | Copying can be expensive on write-heavy paths |
| Single owner plus messages | One worker can serialize all mutations | Adds a queue hop and makes overload visible as backlog |
A token bucket illustrates the choice. Its token count and last-refill time must move together. A small per-bucket lock is often clearer than attempting separate atomic updates. The algorithm trade-offs are covered in the rate-limiting guide.
The following university lecture gives a deeper treatment of interleavings and synchronization primitives:
Concurrency at system boundaries
In system design, the same race appears across database transactions and network retries instead of threads.
Suppose two requests try to reserve the last parking space. Both read available = true, then both write available = false. A mutex inside one API process cannot protect the system when requests reach different processes. The atomic boundary has to move to shared storage:
UPDATE parking_spaces
SET available = false
WHERE id = :space_id
AND available = true;
Exactly one request should observe one affected row. The other must choose another space or report that capacity is gone. The parking-lot design guide follows this race through a complete backend design.
Retries create another concurrency problem. A server can commit a charge while its response is lost, causing the client to retry an operation that already succeeded. An idempotency key gives both attempts one logical identity, but the key check and result write must be atomic. Otherwise the race moves into the deduplication table. See why idempotency is only half the answer for the storage and recovery details.
Use this four-step explanation in a design discussion:
- Name the competing operations.
- State the invariant they could violate.
- Place the atomic boundary in the component shared by every contender.
- Explain the losing path: retry, reject, or compensate.
That sequence is more useful than saying “use a lock,” because it also works for unique constraints, compare-and-set updates, leases, and transactional queues. The technical interview rubric explains how to make that reasoning visible while you code.
FAQ
Does Python’s GIL make a program thread-safe?
No. The GIL limits simultaneous Python-bytecode execution in the standard interpreter, but a logical operation can still span several bytecode instructions and release points. Shared mutable state still needs a synchronization strategy. Threads are useful for I/O-bound concurrency; process pools are a common choice for CPU-bound parallel work.
Why must condition-variable waits use while instead of if?
A wakeup only means the thread should check again. The condition may no longer hold by the time it reacquires the lock, and a wakeup may be spurious. The loop restores the invariant before the operation continues.
Is lock-free code always faster?
No. Under contention, compare-and-set retries can burn CPU and still make poor progress. Lock-free algorithms also cost more to reason about and test. Prefer the simplest mechanism that meets the measured latency and throughput requirement.
How should I practice concurrency questions?
Start with one bounded queue, one ordered-lock transfer, and one database compare-and-set update. For each, trace two competing operations by hand and identify the invariant at every step. That practice transfers better than memorizing isolated definitions.
Comments (0)