Code Review and Working with Existing Codebases Questions
Reviewing others' code and navigating unfamiliar systems: giving and receiving actionable review feedback, spotting correctness and design issues, and reading and understanding large or legacy codebases before changing them. Covers collaborative coding norms, incremental change in shared repositories, and verifying changes against existing behavior. The team-facing side of day-to-day engineering.
Advanced technical domain: A long-running monitoring agent has a memory leak in production. As a reviewer of the agent's codebase, describe the steps you would take to identify leaking code during review: which profilers and CI checks to add, which code patterns to look for (circular refs, global caches, goroutine leaks), and what automated tests or metrics would catch regressions early.
Sample Answer
Direct answer
I would not try to find a production memory leak purely by reading code. I'd combine dynamic evidence, actually profiling the running agent under load, with a targeted code-review checklist for the patterns most likely to cause a slow, steady leak, and then lock in whatever I find with a CI check so the same class of bug can't silently come back.
Structured elaboration
Profilers and dynamic evidence. Before trusting any code-review guess, I'd reproduce the leak under a controlled, repeatable workload and watch memory over time: resident set size (RSS, the actual physical memory the process holds) climbing steadily and never coming back down after garbage collection runs is the signature of a real leak, as opposed to a memory spike that a full garbage collection cycle reclaims. For a compiled agent, a built-in heap profiler (Go's pprof, for example) can take a heap snapshot before and after a workload and show exactly which allocations are still retained; for an interpreted agent, an allocation tracer (Python's tracemalloc, for example) does the equivalent by snapshotting live allocations and diffing two points in time.
Code patterns to look for. The question names circular references specifically, and it's worth being precise about when that's actually the cause: in a language with a tracing garbage collector (which reclaims anything unreachable from a root, cycles included), a circular reference on its own is usually NOT what leaks memory, since the collector can free a cycle nobody outside it points to. What actually leaks in that kind of runtime is something still reachable that shouldn't be:
- Unbounded global caches or maps: a cache or dictionary that only ever grows, with no eviction policy or size cap, is one of the most common leak sources in a long-running service, since every entry is reachable from a live root for the life of the process.
- Leaked lightweight concurrent tasks (goroutines, in Go, or the equivalent worker/coroutine in another runtime): a task that's spawned per request or per event but never exits, most often because it's blocked forever waiting on a channel or queue that nothing will ever write to, keeps itself and everything it captured in its closure alive indefinitely. Every hung task is a small, permanent leak.
- Forgotten deregistration: an event listener, callback, or subscription that's added but never removed when the thing that registered it goes away, so a long-lived object (say, a connection pool) keeps growing a list of listeners that should have been cleaned up.
- Circular references DO matter directly in a reference-counted runtime (no tracing collector, or one that only handles simple cases), where two objects each holding a strong reference to the other can prevent the count from ever reaching zero; if the agent embeds any component like that, it's worth checking specifically.
CI checks and metrics that catch regressions early.
- A memory-regression test: run the agent against a fixed, deterministic synthetic workload (a set number of requests or events, not "for N minutes," so the result is reproducible), take a heap snapshot before and after, and fail the build if retained memory grows past a set threshold.
- A task-leak assertion: after the synthetic workload finishes and drains, assert that the number of live background tasks (goroutines, threads, whatever the runtime calls them) has returned to its known baseline count, not just "isn't growing forever."
- Production metrics and alerting: track RSS trend, heap object count, and live background-task count over time, and alert on sustained upward trend rather than a single spike, since a spike that recovers after garbage collection is expected behavior, not a leak.
Worked example
A concrete pattern this would catch: the monitoring agent spawns one background task per incoming metric batch to forward it to an upstream collector over HTTP, and that HTTP call has no timeout or deadline. If the upstream collector stops responding, every task blocked on that call never returns, and each one keeps its metric-batch buffer alive in its closure. Under normal conditions this is invisible, since tasks come and go quickly; the moment the upstream collector gets slow or unresponsive, the agent starts accumulating one leaked task (and its buffer) per batch, forever, which shows up as a slow, steady RSS climb that never plateaus.
The review-time catch: does every code path that spawns a background task per request or event pass it a context or deadline, so a hung downstream call can't block that task forever? The CI catch: a synthetic test that sends, say, 500 metric batches to a fake upstream collector that never responds, then asserts the live background-task count returns to its pre-test baseline (not zero, since some fixed background workers are expected) once the test workload finishes, rather than staying elevated by roughly 500.
Trade-offs and pitfalls
Continuous production profiling has real overhead, so it's usually run on-demand or sampled, not left on all the time. A CI memory-regression gate with an absolute byte threshold is prone to flaking across different CI runner hardware; a relative threshold (percent growth over baseline, measured on the same run) is more stable. The most common wrong turn is fixing the symptom instead of the cause: bounding a growing cache with a size limit stops the crash but, if entries are evicted on a schedule that doesn't match how they're actually used, can just delay the same leak rather than fix it, so the review should ask why the cache grows unbounded in the first place, not just cap it and move on.
Behavioral: Tell me about a time when you found a critical bug or security issue in infrastructure code during a code review. Use the STAR format: describe the Situation, the Task you had, the Actions you took as reviewer and with the team, and the Results (including any follow-up changes to process or automation).
Sample Answer
Direct answer
I'll walk through a real example from reviewing infrastructure-as-code: catching a security group change that would have opened a database to unrestricted inbound access, and how that turned into both an immediate fix and a lasting change to how the team reviews that class of change.
Structured elaboration
Situation. I was reviewing a routine-looking Terraform PR meant to let a new internal reporting service reach a database. Task. As the reviewer, my job was to catch anything that changed the actual security posture of that database, not just check that the Terraform plan applied cleanly. Action. I noticed the PR's security group rule used 0.0.0.0/0 for the inbound CIDR range (CIDR, Classless Inter-Domain Routing, is the notation for writing a whole block of IP addresses as one value; 0.0.0.0/0 specifically means "every possible IP address"), instead of the reporting service's specific subnet, almost certainly copy-pasted from an example rather than deliberately chosen. Result covers what happened next, both the immediate fix and the longer-term process change, below.
Worked example
I marked the PR as blocking with a specific comment explaining the exposure: this rule would allow any host on the internet to attempt a connection to the database's port, not just the internal reporting service the PR was supposedly scoping access to. I proposed the concrete fix, scoping the rule to the reporting service's actual subnet CIDR instead, and pushed a one-line diff to make it easy for the author to just take. Given the severity, I also flagged it in the team's on-call channel rather than waiting for an asynchronous PR reply, since an already-merged version of a similar mistake elsewhere in the account was worth checking for immediately, not after the PR conversation finished. The PR was updated and merged with the corrected, scoped rule within the hour. Separately, I proposed and helped add an automated policy check (using Open Policy Agent, a policy-as-code tool that can evaluate Terraform plans against written rules) to the CI pipeline that specifically rejects any security group rule opening a sensitive port to 0.0.0.0/0 without an explicit, reviewed exception, so this exact mistake can't reach production again without a deliberate override.
Trade-offs and pitfalls
The judgment call in a story like this is deciding how loudly to escalate: raising it in a live channel instead of just a PR comment was the right call given the actual exposure, but that same urgency would be the wrong tone for a much lower-severity finding, and using it there would just train the team to tune out urgent-sounding messages. A pitfall to watch for when telling this kind of story is stopping at "I found the bug and it got fixed," without the automation follow-up: the more convincing version of this story is one where the process change means the same category of mistake gets caught automatically next time, not just this one instance.
Your team wants to improve the code-review process to reduce blind approvals and improve quality without slowing delivery. Propose concrete process changes, tooling adjustments, and rollout/feedback mechanisms to increase review effectiveness and collaboration.
Sample Answer
Direct answer
To cut blind, rubber-stamp approvals without slowing delivery, I'd fix the conditions that cause rubber-stamping (diffs too large to actually read, unclear expectations, no accountability for what slips through) rather than just tell people to "review harder": smaller pull requests (PRs, proposed code changes submitted for review) nudged by automation, a short reviewer checklist embedded in the PR template, and a feedback loop that closes on real outcomes, piloted before it's rolled out broadly.
Structured elaboration
Diagnose first
Blind approvals usually trace to one of: PRs too large to actually read, unclear expectations about what "reviewed" means, the reviewer having no stake in what slips through, or review being enough of a bottleneck that people route around it.
Process changes
- Require a PR description template (what changed, why, how it was tested), a reviewer with no context defaults to skimming
- Require two reviewers only on genuinely high-risk paths (payments, auth, data migrations), not blanket everywhere, to avoid adding friction where it doesn't earn its cost
- A short reviewer checklist in the PR template (did you trace at least one changed code path, is there a test for the change) that must be acknowledged before approving, not a hard blocker but a nudge that makes rubber-stamping visible to the reviewer themselves
Tooling adjustments
- CI (continuous integration, the automated build/test pipeline) gates lint, type checks, and coverage delta so reviewers spend attention on logic and design, not mechanics
- A diff-size bot that flags, but doesn't block, PRs over a threshold and suggests a split
- Track a "zero-comment approval" rate as a smell to investigate, not to punish
Rollout and feedback mechanism
- Pilot on one team for 2-4 weeks before rolling out org-wide
- Collect qualitative feedback (does this feel like friction, does it feel useful) alongside the metrics
- Keep an escape hatch (an "emergency" label that skips the strict checklist for genuine hotfixes) so the process doesn't get quietly worked around
Worked example
A team pilots this on their payments-adjacent service. Before: 40% of PRs are approved within 10 minutes with zero comments, a rough proxy for rubber-stamping. They add the PR template, the checklist, and a required second reviewer scoped only to payments/ paths via CODEOWNERS (a config file that routes specific paths to required reviewers). After a three-week pilot, zero-comment approvals drop from 40% to 18%, median time-to-merge rises from 6 to 9 hours (a real but survivable cost), and in a retro the team reports catching two logic bugs that previously would have gone through. They keep the change, but scope the two-reviewer rule to only where CODEOWNERS actually flags it, since it was adding delay outside genuinely risky paths without a matching quality benefit.
Trade-offs and pitfalls
- Adding process without removing an existing friction source (large PRs) just slows everything down while blind approvals continue on the same underlying diffs
- A checklist that's too long becomes its own rubber-stamp, people tick boxes without doing the work; keep it to 3-5 items tied to real failure modes
- A blanket two-reviewer requirement is one of the fastest ways to slow delivery for marginal quality gain; scope it to genuinely risky code
- Watch for a "must comment" norm getting gamed with a low-value nit just to look engaged
Technical debugging: review the following Python snippet intended to concurrently fetch metadata for hosts. Identify race conditions and concurrency issues, and propose a corrected, thread-safe implementation with reasoning.
import threading
hosts = ['a','b','c']
results = []
def fetch(h):
data = get_metadata(h)
results.append((h, data))
threads = []
for h in hosts:
t = threading.Thread(target=fetch, args=(h,))
threads.append(t)
t.start()
for t in threads:
t.join()
print(results)
Assume get_metadata may raise exceptions and is non-blocking I/O bound.
Sample Answer
Direct answer
The real problem isn't a low-level data race on results.append (a single list append happens to be safe from internal corruption in CPython for this simple case). The real problem is that this code has no way to catch a per-host exception: if get_metadata raises inside fetch, that entire thread dies silently, its host's result never makes it into results, and neither the join() calls nor the final print reveal that anything went wrong. Replace hand-rolled threading.Thread objects with a ThreadPoolExecutor and Future-based error handling, and key the results by host in a dictionary, which is what actually makes this both thread-safe and debuggable.
Structured elaboration
Approach. A race condition is an outcome that depends on unpredictable timing between threads. Here, the more serious issue is a swallowed exception, not data corruption: none of the threads' exceptions are caught anywhere, so a single failing host silently vanishes from the output with no error and no trace. There's also no bound on concurrency, one raw thread is spawned per host with no limit, which doesn't scale if hosts ever has thousands of entries instead of three.
Key points. concurrent.futures.ThreadPoolExecutor manages a bounded pool of worker threads instead of spawning one thread per item. Each submitted call returns a Future, and calling .result() on it either returns the value or re-raises whatever exception the underlying function raised, which is exactly what makes per-host errors visible instead of silent. Writing results into a dict keyed by host, rather than appending tuples to a shared list, avoids any ambiguity about which result belongs to which host regardless of completion order.
Worked example
import concurrent.futures
def get_metadata(h):
if h == 'b':
raise ValueError(f"no metadata for host {h}")
return {"host": h, "region": "us-east-1"}
def fetch(h):
data = get_metadata(h)
return h, data
hosts = ['a', 'b', 'c']
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as ex:
futures = {ex.submit(fetch, h): h for h in hosts}
for fut in concurrent.futures.as_completed(futures):
h = futures[fut]
try:
host, data = fut.result()
results[host] = data
except Exception as e:
results[h] = {"error": str(e)}
for h in sorted(results):
print(h, "->", results[h])
This uses a stub get_metadata that deliberately fails for host b, so the demo exercises both the success and failure paths. Output (printed in sorted host order, so it's stable across runs regardless of which thread finishes first):
a -> {'host': 'a', 'region': 'us-east-1'}
b -> {'error': 'no metadata for host b'}
c -> {'host': 'c', 'region': 'us-east-1'}
Host b's failure is now visible as {'error': 'no metadata for host b'} in the output, instead of silently disappearing the way it would in the original code.
Complexity
O(n) total work across n hosts, bounded to at most max_workers requests in flight at once, rather than n simultaneous raw threads for however large hosts happens to be.
Edge cases
An exception from get_metadata is now captured and reported per host instead of silently vanishing, as demonstrated above. An empty hosts list makes the loop do nothing and produces an empty result, with no error. A host that takes far longer than the others doesn't block collecting the ones that finish first, since results are consumed via as_completed in actual completion order, not submission order.
Trade-offs and pitfalls
ThreadPoolExecutor is the right tool for I/O-bound work waiting on network calls, which is what the question states get_metadata is. For CPU-bound work, Python's global interpreter lock means threads don't actually run Python bytecode in parallel, so a process pool, or a different approach entirely, would be needed instead. At genuinely large scale, many thousands of hosts, asyncio avoids the per-thread memory and context-switching overhead altogether; that's worth knowing as the next step up, not something every small fetcher like this one needs from day one.
Design a set of static analysis rules to detect common concurrency bugs in a Java codebase. Provide at least four rules, explain the detection heuristic and likely false positives for each, and suggest mitigations.
Sample Answer
Direct answer
Design this as four independent, mechanical checks over the code's abstract syntax tree (AST, a tree representation of parsed source code a tool can walk programmatically), each targeting one well-known Java concurrency bug pattern, rather than trying to prove general thread-safety, which isn't something a static tool can fully decide. The four rules: an unsynchronized mutable field shared across threads, double-checked locking without volatile, a non-thread-safe collection shared across threads without synchronization, and a silently swallowed InterruptedException. Each rule trades some false positives for being cheap and mechanical to run in continuous integration (CI).
Structured elaboration
Rule 1: unsynchronized mutable shared field. Heuristic: find non-final, non-volatile fields that are written in one method and read in another that can run on a different thread (a public method, a Runnable/Callable implementation, a listener callback). Flag if no synchronization (a synchronized block or an explicit Lock) guards the reads and writes, and the field carries no documented @GuardedBy annotation (a marker showing which lock protects that field, so a reader does not have to guess). False positives: a field that's set once during safe, single-threaded initialization before being published, or one only ever touched under an external framework lock the tool can't see. Mitigation: mark the field final or volatile, encapsulate access behind a synchronized method or lock, or use an Atomic* type from java.util.concurrent.atomic.
Rule 2: double-checked locking without volatile. Heuristic: detect the lazy-initialization idiom, a null check, a synchronized block, then a second null check, on a field that isn't declared volatile. False positives: rare cases where construction is genuinely safe to publish without one, but this is easy to get wrong, so the rule should default to flagging it. Mitigation: declare the field volatile, or switch to the initialization-on-demand holder idiom, or use java.util.concurrent.atomic.AtomicReference.
Rule 3: non-thread-safe collection shared across threads. Heuristic: find a java.util.ArrayList, HashMap, or similar mutable, non-thread-safe collection whose mutating methods (add, put, remove) are called from more than one method reachable by different threads, without synchronization. False positives: a collection that's locally scoped, guarded by a higher-level lock elsewhere, or effectively immutable after construction. Mitigation: replace with ConcurrentHashMap, CopyOnWriteArrayList, Collections.synchronizedList(...), or make the collection immutable after building it.
Rule 4: swallowed InterruptedException. Heuristic: a catch block for InterruptedException that's empty, only logs, or rethrows as an unrelated exception, without restoring the thread's interrupt status via Thread.currentThread().interrupt(). False positives: test code, or a framework-specific handler that deliberately converts the exception into a controlled shutdown. Mitigation: restore the interrupt status if not rethrowing the original exception, and exit any loop promptly rather than continuing to run after an interrupt was requested.
Worked example
Rule 2 applied end to end, since it's the most concrete of the four:
// Flagged: double-checked locking without volatile
private Connection instance;
public Connection get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = new Connection();
}
}
}
return instance;
}
// Fixed: mark the field volatile so a write becomes visible to other
// threads before the reference is published
private volatile Connection instance;
Why this matters: without volatile, another thread can, under the Java memory model (the rules that define when one thread's writes become visible to another thread), observe a non-null reference to instance before the constructor's writes to that object are actually visible to it, a partially-constructed object leaking out. This is a real, documented hazard in the Java memory model, not a hypothetical one, and it's exactly the kind of bug that can pass every functional test and still fail intermittently in production under load.
Trade-offs and pitfalls
These rules are local and syntactic, so none of them can reason across method or class boundaries, a lock acquired somewhere else in the call chain won't be seen, which means real false negatives on anything indirect are expected, this is a floor, not a guarantee. Tune the false-positive rate by supporting a documented @GuardedBy annotation or a suppression comment that requires a stated justification, so the rule stays actionable instead of getting bulk-suppressed the first time it's noisy. Prioritize findings by realistic blast radius (how much damage this specific bug could do if it shipped, not just whether it technically exists), a shared field in a class used by every incoming request is worse than the same pattern in a rarely-instantiated helper, rather than treating every hit as equally urgent. Before building all four from scratch, check what an existing, actively maintained tool already covers: SpotBugs, the maintained successor to the now-unmaintained FindBugs, and Google's Error Prone both catch some of these patterns out of the box.
Unlock Full Question Bank
Get access to all Code Review and Working with Existing Codebases interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.