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.
List five types of automated checks you would want running before a human ever looks at the code, and explain why each one earns its place in the pipeline. Which would you consider mandatory, and which optional?
Sample Answer
Direct answer
Before a human looks at the code, I want checks that are cheap, deterministic, and fast: build/compile, the existing test suite, correctness-oriented linting, formatting, and security or secret scanning. Build, tests, correctness linting, formatting, and security scanning are effectively mandatory since each is cheap and prevents real harm; a coverage-delta threshold is the one I'd keep optional or advisory.
Structured elaboration
| Check | Why it earns its place | Mandatory or optional |
|---|---|---|
| Build or compile | Nothing else is worth reviewing if the code doesn't build; catches syntax and type errors instantly | Mandatory |
| Existing test suite | Confirms the change didn't break behavior the team already relies on; a human can't hold hundreds of existing test cases in their head | Mandatory |
| Linting for correctness patterns (unused variable, unreachable code, obvious bug shapes) | Catches whole classes of bugs a quick human skim easily misses, for free, every time | Mandatory |
| Auto-formatting or a format check | Removes style disagreement entirely from human review, the single biggest source of low-value review comments | Mandatory |
| Security or secret scanning (hardcoded credentials, known-vulnerable dependency versions) | A missed hardcoded secret or vulnerable dependency is a real incident, and exactly the kind of thing that's easy to miss skimming a large diff | Mandatory |
| Coverage delta (does the pull request (PR, a proposed code change) add tests proportional to the code it adds) | Useful signal, but a blunt one; a PR can legitimately have low delta coverage, for example a config-only change | Optional / advisory |
Why this ordering matters
Machines are fast, consistent, and never get tired or skip a check under time pressure, exactly the trait humans lose in a rushed review. This reframes the human reviewer's job: not "did the tests pass," but "does this change make sense, is the design right, are there edge cases the tests don't cover."
Worked example
A PR adds a new /export endpoint. Continuous integration (CI, the automated build/test pipeline) runs: the build passes; the existing 340-test suite passes; linting flags an unused import, requiring a one-line manual fix; the secret scanner catches a hardcoded key accidentally left in a test fixture and blocks the merge until the author replaces it with an environment-variable reference; the coverage-delta check shows the new endpoint added 0 new tests against 45 new lines and posts an advisory warning, not a block, prompting the author to add a test before requesting review. Only after the build, existing tests, and the secret-scan block clear does the PR reach a human reviewer, who focuses entirely on whether the endpoint's authorization check is correct, something none of the automated checks could evaluate.
Trade-offs and pitfalls
- Too many mandatory gates slows every PR for marginal benefit; keep the mandatory set to checks that catch real, expensive-to-miss problems, not personal style preferences
- A coverage-delta threshold enforced as a hard block gets gamed with low-value tests written just to hit a number; keep it advisory and let a human interpret it
- Automated checks can produce false confidence, "CI is green" means the mechanical bar was cleared, not that the design is right
- Security or secret scanners produce false positives; if they cry wolf too often, engineers start ignoring or bypassing them
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.
You're the on-call reviewer for a hotfix that must be reviewed and merged in under two hours. The author asks for rapid approval. Walk through exactly what you'd check in this timeboxed emergency review to convince yourself the fix is safe to ship, and what deployment precautions you'd still insist on.
Sample Answer
Direct answer
In a two-hour emergency review I compress scope, I don't skip verification. I read the diff for correctness and blast radius, confirm the fix is covered by a test, and get deployment safety nets in place (a fast rollback path, a way to disable the change without a full redeploy) before I approve. Speed comes from narrowing what I review, not from reviewing less carefully.
Structured elaboration
What I check in the diff itself
- Read every changed line, not a skim: what changed, and does it match the actual production symptom (the incident ticket or error log), not just a plausible-sounding fix
- Blast radius: does the change touch only the failing code path, or does it also touch shared logic other features depend on
- Does the fix address the root cause or just the symptom that's visible right now
- Does the diff add or update a test that reproduces the original failure
Managing the two hours
- Spend the first 10-15 minutes understanding the actual symptom before opening the diff, so I can judge whether the fix's scope matches the problem
- Defer all style/nit comments to a follow-up, a hotfix review is not the place for them
- If the diff does more than the minimal fix, ask the author to strip it back rather than review the extra scope under time pressure
Deployment precautions I still insist on
- A feature flag or config toggle if the codebase supports one, so the fix can be disabled without a redeploy
- A staged rollout: canary to a small percentage of traffic or a subset of instances first, not straight to 100%
- Confirmed monitoring on the specific metric this fix touches, and a named person watching it after deploy
- A fast, tested rollback path (previous build artifact ready, or a revert that's known to apply cleanly)
Worked example
Say the incident is a crash in checkout caused by code that reads a promo-code field without checking whether it was ever set. The diff adds a check before reading that field. I check: (1) the check covers the empty case, but does it also cover a malformed-but-present value that could crash the same way, if not, I ask why; (2) a unit test asserts the code path with an empty promo code completes without crashing; (3) the diff is 8 lines, a scope I trust. I approve, but I ask for a canary deploy to 5% of traffic for 15 minutes with the crash-rate dashboard open before full rollout, and I confirm the previous build is one click away to redeploy if the canary shows a new problem.
Trade-offs and pitfalls
- The biggest pitfall is reviewing the diff instead of the failure: always tie the fix back to the actual symptom, or you can approve something that's plausible but wrong
- Pressure to approve fast is exactly how rubber-stamping happens; a two-minute skim under a deadline is not a review
- Don't cut the rollback plan to save time, that safety net is what makes reviewing fast responsibly possible at all
- A hotfix that ships without its own test is debt that tends to regress on the next change to that code
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.
Timeboxing reviews helps keep velocity high. What is your personal timebox for reviewing a small PR (under 200 lines) and a medium PR (200-1000 lines)? Describe how you triage and prioritize which files or changes to review first within the timebox.
Sample Answer
Direct answer
I timebox a small PR (under 200 lines) to about 15 to 25 minutes, and a medium PR (200 to 1000 lines) to 45 to 75 minutes, splitting the medium one into two focused passes if it's going to run past an hour. Inside that time, I triage by risk, not by file order: public interfaces, data changes, and security-relevant code get looked at first, cosmetic changes get looked at last, if at all.
Structured elaboration
Timebox. Small PR: 15 to 25 minutes. Medium PR: 45 to 75 minutes, and if I'm not going to make that, I split it into two shorter, focused sessions rather than pushing through fatigued, since review quality drops noticeably in a single overlong sitting.
Triage order within the timebox.
- Read the PR description and linked ticket first, a couple of minutes, to know what the change is actually supposed to do before reading a single line of diff.
- Check CI status; if it's failing, that's the first comment, before spending time on anything else.
- Scan the full diff at a high level to understand its shape and which files matter most.
- Read the highest-risk files first: anything touching a public interface, a database migration, authentication or authorization code, in that rough order.
- Then core logic changes, then whether tests actually cover the new behavior.
- Cosmetic and configuration changes last, and only if time remains.
If I run out of time with something still unresolved: either ask the author for a short synchronous walkthrough of just the confusing part, or explicitly mark specific items as a follow-up rather than silently approving with unresolved questions.
Worked example
A 600-line PR (medium) refactors an authentication middleware and adds a new permission check. I timebox 60 minutes. Description and ticket: 3 minutes. CI is green. High-level scan: 5 minutes, and I notice the diff touches both the middleware itself and an unrelated logging format change bundled into the same PR. I review the middleware and permission-check logic first, about 30 minutes, since that's the security-relevant core of the change, and leave a blocking comment about a missing check on one code path. I check test coverage for the new permission check next, about 15 minutes, and confirm it's covered. With about 7 minutes left, I glance at the unrelated logging change, decide it's low-risk and clearly separable, and leave a non-blocking note suggesting it ship as its own PR next time rather than reviewing it in depth now.
Trade-offs and pitfalls
A strict timebox risks under-reviewing something genuinely subtle that happens to be small in line count, since risk and diff size aren't the same thing; the fix isn't abandoning the timebox, it's being willing to extend it explicitly and say why, rather than silently rushing through a risky small change to stay inside an arbitrary number. The most common pitfall is reviewing files in the order they happen to appear in the diff instead of by actual risk, which means a time-pressured reviewer can run out of time having thoroughly reviewed formatting changes while barely glancing at the one file that actually mattered.
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.