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.
You are responsible for integrating static-analysis tools for a compiled monitoring agent written in Go. As a reviewer, which tools/checks would you require in CI and which classes of defects do they catch (formatting, race conditions, undefined behavior, performance issues)? Explain how to prioritize fixes found by these tools in PR reviews.
Sample Answer
Direct answer
I'd require a small, layered set of tools in CI rather than one do-everything linter: an auto-fixable formatter for style, a vet-style correctness checker for undefined-behavior-class bugs, the language's built-in data-race detector for concurrency bugs, and benchmarks for performance regressions. Then I'd prioritize fixes by what class of defect they represent, not by which tool happened to report it: races and correctness bugs block the merge, performance and style don't.
Structured elaboration
Formatting. Go ships a canonical formatter (gofmt, plus goimports for import ordering) that produces one deterministic output for any given source file. Running this in CI and failing on any diff removes formatting from human review entirely, since there's no room for a style opinion once the formatter is authoritative.
Undefined behavior and correctness. go vet catches a specific, well-known set of suspicious constructs (a Printf-style call whose format string doesn't match its arguments, a struct copied by value that embeds a mutex, which silently breaks that mutex's locking guarantee for the copy). staticcheck goes further: unreachable code, an unused error return, or a comparison that can never be true.
Race conditions. Go's toolchain has a built-in data race detector (a data race is two goroutines, Go's lightweight concurrent tasks, accessing the same memory at the same time with at least one of them writing, and no synchronization between them), invoked with go test -race. This instruments the binary and reports real races observed while the tests run; it can't find a race that no test path exercises, so it's only as good as test coverage of the concurrent code paths.
Performance. Go's benchmark tooling (go test -bench, combined with the same heap/CPU profiler used for leak hunting, pprof) surfaces regressions when compared against a stored baseline, rather than trying to guess at performance from source alone.
Prioritizing fixes in review. I'd rank findings by consequence, not by tool:
- Blocking: data races, and any
go vet/staticcheckfinding that represents an actual correctness bug (the mutex-copy example above is a real bug, not a style nit). - Strongly recommended before merge: ignored errors on operations that can fail meaningfully, and confirmed performance regressions with a benchmark to back them up.
- Follow-up acceptable: non-blocking staticcheck suggestions, dead code that adds noise but no risk.
- Auto-fixed, not discussed: formatting and import ordering, since a human should never spend review time on something a formatter already fixed for them.
Worked example
A PR modifies a shared Stats struct that's passed around by value and embeds a sync.Mutex. go vet flags this: copying a struct that embeds a lock triggers its copylocks check, warning that the assignment copies a lock value. That's not a style nit, it's a real bug: copying a struct that contains a mutex creates a second, independent lock, so code holding the copy's lock provides no actual protection against code holding the original's lock, and the two sides can now race on the data the mutex was supposed to protect. Separately, go test -race flags a genuine concurrent map write elsewhere in the same PR, and gofmt reports three files with import-order diffs.
Prioritization: the mutex-copy bug and the concurrent map write are both blocking, since both are real correctness/concurrency bugs, not opinions; the import-order diffs get auto-fixed by running goimports -w and are never discussed in review at all.
Trade-offs and pitfalls
Running -race on every test invocation roughly doubles memory use and meaningfully slows the run, so most teams run it in a dedicated CI lane rather than on every local go test. Turning on a strict linter for the first time in a legacy repo produces a wall of pre-existing findings that block every future PR unless you baseline them (only fail on NEW findings in changed lines) rather than demanding the whole codebase get fixed at once. The most common pitfall in review is treating every tool finding as equally urgent: burying a genuine data race in the same list as ten cosmetic suggestions makes it likely the race gets the same "eh, later" treatment as the cosmetics.
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.
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.
When reviewing test code, what distinguishes a high-quality test from a brittle or misleading test? Walk through the checks you'd perform on tests in a PR, with a one-sentence rationale for each.
Sample Answer
Direct answer
A high-quality test fails when, and only when, the behavior it's supposed to protect actually breaks. A brittle test fails for unrelated reasons, a refactor, timing, execution order, and a misleading test passes even when the behavior is broken. Reviewing test code means checking specifically for those two failure modes, not just confirming a test exists.
Structured elaboration
| Check | Rationale |
|---|---|
| Does it test behavior, not implementation? | A test asserting a private internal variable's exact value, instead of the function's observable output, breaks on every harmless refactor even when nothing user-visible changed |
| Would it actually fail if the logic broke? | Mentally invert the line of code the test is supposedly protecting; a test that would still pass with the real logic removed gives false confidence, which is worse than no test at all |
| Is it deterministic? | A test depending on wall-clock time, real network calls, unseeded randomness, or execution order will flake, and people learn to re-run and ignore flaky tests rather than trust them |
| Does the assertion match the intent? | "No exception was thrown" is a much weaker check than asserting the actual expected value; the assertion should check the specific behavior the PR (pull request) describes |
| Is the test isolated? | Tests sharing mutable state (a global, a shared database row) with other tests can pass or fail depending on run order, making a real failure hard to reproduce |
| Is the failure message useful? | A message that says what was expected versus what actually happened saves the next debugger from re-deriving what the test was even checking |
Worked example
A PR adds a test for a discount-calculation function. Weak version: assert calculate_discount(100, 0.1) is not None, which passes even if the discount math is completely wrong, as long as something is returned. Strong version: assert calculate_discount(100, 0.1) == 90, which fails immediately if the discount logic is wrong, plus a boundary case, calculate_discount(100, 0) == 100. Tracing it: if the discount subtraction were removed entirely (the function just returned the input unchanged), the weak assertion would still pass, but the strong one would fail right away, confirming the strong version actually tests the behavior it claims to.
Trade-offs and pitfalls
Line-by-line scrutiny of every test on every PR isn't realistic; prioritize new business logic and edge cases, and be more lenient on straightforward tests for simple getters and setters. A common wrong turn is treating "there's a test" as sufficient without checking whether it would actually catch a real regression, which is the same blind spot a raw test-coverage percentage has, since a line can be "covered" by a test that never meaningfully asserts anything about it.
When reviewing code for readability and maintainability, what concrete signs do you look for? Provide at least five aspects, and for each give a short example of a red flag and a suggested improvement.
Sample Answer
Direct answer
Readability review isn't about taste, it's checking whether the next reader, often the same author months later, can understand intent quickly. Five concrete, checkable signs: naming, function length and single responsibility, nesting depth, magic numbers, and comment quality.
Structured elaboration
| Aspect | Red flag example | Suggested improvement |
|---|---|---|
| Naming | A name that doesn't say what it holds or does, e.g. d, data2, doStuff() | Rename to describe intent, e.g. daysSinceLastLogin, retryFailedUploads() |
| Function length and single responsibility | A 150-line function doing validation, a database call, and response formatting all at once | Split into named steps, e.g. validateInput, saveRecord, formatResponse, each readable and testable on its own |
| Nesting depth | Four or five levels of nested if/for blocks that require holding a lot of context to follow | Use early returns or guard clauses to flatten the happy path, e.g. if not valid: return at the top instead of wrapping the rest of the function in an if valid: block |
| Magic numbers and strings | A bare literal like if status == 3 with no explanation of what 3 means | A named constant or enum, e.g. if status == OrderStatus.SHIPPED |
| Comment quality | A comment restating obvious code, e.g. // increment i by 1 above i += 1 | Remove obvious comments; reserve comments for non-obvious reasoning, e.g. why a retry happens once because an upstream API is known to drop the first request after a deploy |
Worked example
A function checking discount eligibility starts deeply nested, with a magic number and an unclear name:
def check(u, o):
if u.active:
if o.total > 0:
if o.status == 3:
return True
else:
return False
else:
return False
else:
return False
Applying the checklist: rename to isEligibleForDiscount(user, order), replace 3 with OrderStatus.SHIPPED, and flatten with early returns:
def is_eligible_for_discount(user, order):
if not user.active:
return False
if order.total <= 0:
return False
return order.status == OrderStatus.SHIPPED
Same logic, but each condition is now readable on its own line without tracking four levels of nesting.
Trade-offs and pitfalls
Readability review can drift into pure style bikeshedding; the fix is pushing formatting to a linter and reserving human judgment for naming, structure, and comment quality, which a linter can't evaluate. Over-refactoring into a dozen tiny named steps can also hurt readability by scattering logic across too many indirections; the goal is clarity, not a rule that shorter is always better.
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.