Introduction

Code Review Interviews

The code review round - what it tests (threading, concurrency, memory, runtime depth), how to approach a snippet, and the signals that separate a pass from a strong pass.

Free~10 min

The code review round is the format where the interviewer hands you a snippet - sometimes a real bug from their codebase, sometimes a synthetic example - and asks you to find the bug, explain the root cause, and discuss the fix. It tests depth that algorithmic and coding-challenge rounds do not reach: threading, concurrency, memory model, runtime internals, and your ability to reason about a system you did not write.

A candidate who aces LeetCode can fail this round. The skills are different: you are not inventing a solution, you are diagnosing one. The interviewer wants to see how you read code, how deep you can go, and whether you can tell the difference between a real bug and a style complaint.

What they are testing

AreaWhat the interviewer wants to see
ThreadingCan you spot a race condition, a deadlock, a missing lock, a visibility guarantee that does not hold?
ConcurrencyCan you reason about ordering, atomicity, and what happens when two threads interleave?
MemoryCan you find a leak, a stale reference, an off-by-one in a buffer, a use-after-free?
Runtime / VMCan you explain what the language runtime does: GC, escape analysis, integer overflow, string interning?
Systems reasoningCan you discuss the trade-off between correctness and performance, and name when you would accept a weaker guarantee?

The round is not about finding the most bugs. It is about finding the right bug, naming its root cause precisely, and discussing the fix at increasing depth. An interviewer who keeps asking "and what else?" is testing whether you can go deeper - that is a good sign, not a sign you missed something.

How to approach a snippet

1. Read the whole thing first

Do not start naming bugs on line 2. Read the entire snippet once, silently, and form a hypothesis about what it is trying to do. Then state it: "This looks like a thread-safe counter with lazy initialization. The bug is likely in the initialization path." This frames the rest of the conversation.

2. Name the root cause, not the symptom

A symptom is "the counter is sometimes wrong." A root cause is "the double-checked locking pattern reads instance without a memory barrier, so thread B can see a non-null reference before the constructor has finished writing its fields." The second answer is what passes the round.

Work from symptom to root cause explicitly:

  1. What goes wrong? Name the observable behavior.
  2. When does it happen? Name the interleaving or condition.
  3. Why does the language allow it? Name the memory model, the visibility guarantee, or the atomicity gap.
  4. What is the fix? Name the minimal change and what guarantee it restores.

3. Discuss the fix at increasing depth

After you name the root cause, the interviewer will ask how you would fix it. Give the minimal fix first - the one-line change that removes the bug. Then discuss the deeper fix: the structural change that makes the class of bug impossible, not just this instance.

For a race on a shared counter:

  • Minimal fix: add a lock around the increment.
  • Deeper fix: use an atomic integer, which removes the lock and the contention, and makes the atomicity guarantee visible in the type.
  • Deepest fix: redesign so the counter is not shared - per-thread counters aggregated on read, which removes the contention entirely.

The interviewer is not looking for the deepest fix. They are looking for your ability to walk down the depth ladder and explain the trade-off at each step.

Where the common bugs live

Threading and concurrency

  • Race condition: two threads read-modify-write a shared field without synchronization. The fix is a lock, an atomic, or removal of the shared state.
  • Deadlock: two locks acquired in opposite orders in two threads. The fix is a global lock ordering or a single lock.
  • Double-checked locking: the classic Java pitfall. The reference can be non-null before the constructor finishes. The fix is volatile (Java 5+) or an inner-class holder.
  • Visibility: thread A writes a flag, thread B reads it, but B does not see the write because there is no happens-before relationship. The fix is a memory barrier (volatile, synchronized, Atomic*).
  • Wait/notify without a loop: wait() can wake up spuriously. The fix is to always wait inside a while (condition) wait() loop.

Memory

  • Leak via a long-lived collection: a static map holds references to objects that are never removed. The fix is a WeakHashMap or explicit removal.
  • Stale reference in a callback: a listener is registered but never unregistered, keeping a large object alive. The fix is removal on teardown.
  • Off-by-one in a buffer: copy(src, dst, len) where len is the source length but dst is smaller. The fix is to bound on the destination.
  • Use-after-free / close: a file or connection is closed but a reference is still held and used. The fix is to null the reference or scope the resource with try-with-resources.

Runtime and language

  • Integer overflow: a + b overflows silently in most languages. The fix is Math.addExact (Java), checked arithmetic (Swift), or a wider type.
  • String identity vs equality: == on strings compares references, not values. The fix is .equals(), or intern the strings if you control both sides.
  • Autoboxing: Map<Integer, V> with int keys can create extra Integer objects. The fix is a primitive-specialized map or Integer.valueOf reuse.
  • Escape analysis: a local object escapes into a shared field, defeating stack allocation. The fix is to not share it, or to make it explicitly immutable.

Signals to demonstrate

SignalWhat it looks like
PrecisionYou name the exact line, the exact interleaving, the exact guarantee that fails.
DepthYou can go from symptom to root cause to minimal fix to deeper fix without prompting.
Trade-off awarenessYou can say "this fix adds contention; that fix removes the shared state but adds allocation; I would choose this one because..."
RestraintYou do not rename variables, reformat, or propose style changes that do not fix a bug.
HonestyWhen you do not know, you say "I am not sure about the visibility guarantee in this language; let me reason from first principles."

Common mistakes

  • Naming style issues as bugs: "this variable name is unclear" is not a bug. The interviewer wants a defect, not a lint pass.
  • Jumping to the fix before naming the root cause: the interviewer cannot evaluate your diagnosis if you skip it. State the root cause, then the fix.
  • Staying at the symptom level: "it crashes" is a symptom. Keep going until you reach the language or system guarantee that fails.
  • Over-fixing: rewriting the snippet is not the task. Find the bug, name the minimal fix, discuss the deeper fix. Do not redesign the class.
  • Guessing the language runtime: if you do not know whether the language guarantees atomicity for a field, say so and reason about both cases. A wrong claim about the runtime is worse than an honest "I would check the spec."

How to prepare

The round rewards depth over volume. Pick a language you will interview in and learn its memory model cold: what is atomic, what is visible, what is ordered. Then practice on real bugs - open-source issue trackers are a good source, especially concurrency-related issues with minimal reproducers.

A useful drill: take a small concurrent class you wrote, remove one lock or one volatile, and explain exactly what can break, when, and why. If you cannot, you do not yet understand the guarantee that the keyword was providing.

Practice table

Practice articles for the code review round are coming. For now, use the coding-challenge interviews to build the state-model intuition that makes spotting a bug faster, and the interview protocol for the in-room conversation.

Done reading? Mark it so it sticks in your dashboard.

Next in CodingArrays