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.
As part of a code review, you are asked to ensure that a change respects the team's API stability policy. Describe how you would verify backwards compatibility, what kind of tests you'd want in place, and how you'd document and communicate an intentional breaking change to downstream consumers.
Sample Answer
Direct answer
Treat this review as three linked jobs: prove the change doesn't break the existing contract, or clearly flag that it intentionally does; have tests that catch a break automatically rather than relying on a human noticing during review; and if it IS a breaking change, run an explicit communication and coordination process across every downstream team before it ships, not just a changelog entry after the fact.
Structured elaboration
Verify backward compatibility. Diff the public API (application programming interface) surface, endpoints, request and response schemas, public method signatures, against the previous version using an automated schema-diff tool. Check that the semantic-versioning bump (version numbers like 2.1.0, where the first number only increases for a breaking change) actually matches the impact: a removed field or endpoint is a major-version break, not a minor one. Manually scan for the classic breaking patterns an automated diff might present too dryly to flag on its own: a field that used to be optional becoming required, a removed enum value, or a changed error code that a consumer's error-handling logic depends on.
Tests to require. Unit and integration tests for the new behavior, plus consumer-driven contract tests: each known consumer publishes a "contract" describing what it expects from this API, and the provider's continuous integration (CI) pipeline runs every consumer's contract against the new code before merge, so a break shows up as a failing build instead of a production incident weeks later.
Documenting and communicating an intentional break. If a break is genuinely necessary, it needs a migration guide with concrete before-and-after request or response examples, a deprecation timeline (announce now, remove after an agreed grace period), and a version bump that signals the change to anyone watching semantic versioning.
The multi-team angle. This is the part that's easy to skip. Different downstream teams often run on different release cadences: one team deploys daily, while another ships a mobile client that goes through app-store review on a multi-week cycle. "We announced it in the changelog" is not the same as "every consumer had a realistic window to migrate before the old behavior disappeared." The reviewer's job includes checking that the deprecation window is long enough for the SLOWEST consumer's release cadence, not the average one, and that there's a direct, tracked notification, not just a changelog line, to every known downstream team: who owns each consumer, has that owner acknowledged the timeline, and is there a release-coordination step, such as a shared rollout calendar or a required sign-off from each consuming team, before the old behavior is actually removed.
Worked example
A concrete scenario: a payments API removes a deprecated legacy_currency_code field. Applying the process: an automated schema diff confirms the field is gone, which requires a major-version bump. Contract tests run for the three known consumers (a web checkout, a mobile app, and a partner integration), and the partner integration's contract still references the old field, so its test currently fails, that failure is the exact signal that should hold the release, not something to work around. A migration guide is written showing the old request shape next to the new one. The deprecation timeline is set to match the partner integration's slowest release cadence, which, per that team, needs six weeks of notice because their deploys go through a change-advisory process, not the web team's daily-deploy pace. Each of the three consuming teams gets a direct message naming the field, the deadline, and a link to the migration guide, and the field's actual removal is gated on each team explicitly acknowledging the change, not merely being notified of it.
Trade-offs and pitfalls
Coordinating across three teams' release cadences is slower than shipping the break and hoping consumers keep up, but the alternative, a downstream team's production system silently breaking because nobody saw a changelog entry, is far more expensive to unwind afterward. A common pitfall is confusing "we deprecated it" with "it's now safe to remove": the deprecation period only did its job if every consumer actually acted on it, which needs active follow-up, for example a dashboard tracking remaining callers of the old field, rather than passively waiting out the clock.
You are reviewing a new REST endpoint PR. Describe a complete test strategy to validate behavior, including unit tests, integration tests, contract tests, load tests, and edge case tests. For each test type give a concrete example case that must be covered and what the reviewer should verify in the test code.
Sample Answer
Direct answer
Reviewing a new REST (Representational State Transfer, the common web API style) endpoint's test coverage means checking five distinct layers: unit tests for the endpoint's own logic in isolation, integration tests against real dependencies, contract tests that pin the API's shape for consumers, load tests for realistic traffic, and explicit edge-case tests. Each layer catches a different class of bug, and none of them substitutes for the others.
Structured elaboration
| Test type | Concrete example case | What the reviewer verifies |
|---|---|---|
| Unit tests | Given a valid request body, the handler returns the expected response shape and status code; given an invalid field, it returns a 400 with a specific error message | The test exercises the real logic, not just that a mock was called, and covers both the success path and at least one validation failure |
| Integration tests | Create a resource via the endpoint, then confirm it's actually persisted and retrievable via a separate read call | The test uses a real or realistic data layer, not mocks, so it would catch a broken query or a schema mismatch |
| Contract tests | A field a known consumer depends on, e.g. createdAt, must always be present and always an ISO 8601 timestamp string; the test fails if a future change removes or reshapes it | The contract covers every field an actual known consumer depends on, not just a generic happy-path shape check |
| Load tests | Simulate the expected peak requests-per-second and confirm the p95 latency, the 95th-percentile response time, meaning 95% of requests are at least this fast, stays under the team's target with near-zero error rate | The load profile reflects realistic expected traffic, and the test checks a latency or error threshold, not only that requests completed |
| Edge case tests | An empty request body, a request at the maximum allowed payload size, a duplicate submission with the same idempotency key (a client-generated ID sent with a request so retrying it doesn't create a duplicate), a request from an unauthenticated caller | Each case has an explicit assertion about expected behavior, a specific status code and body, not just "doesn't throw an unhandled exception" |
Worked example
A PR (pull request) adds POST /orders. Unit test: a valid order payload returns 201 with an order ID; a missing items field returns 400. Integration test: after the POST, a subsequent GET /orders/{id} returns the same order from the real test database. Contract test: the response always includes id, status, and createdAt in the shape the frontend's order-confirmation page depends on. Load test: simulate the expected peak checkout traffic and confirm p95 latency and error rate stay within the team's targets. Edge cases: an empty items array, a duplicate submission with the same idempotency key that should not create two orders, and a request with an expired auth token that should return 401, not 500.
Trade-offs and pitfalls
Not every endpoint needs all five layers at full weight, a low-traffic internal admin endpoint may not need load testing, and a purely internal endpoint with no external consumers may not need a formal contract test. Calibrate to the endpoint's actual risk and consumers rather than mechanically requiring every category on every PR. A common wrong turn is treating a high unit-test count as sufficient while skipping integration tests entirely; unit tests with everything mocked can pass while the real database query underneath is broken.
Your team's review turnaround time increased from 8 hours to 72 hours and post-release defects doubled in the same quarter. Perform a root-cause analysis: list hypotheses across people, process, and tooling, describe data you would collect to confirm each hypothesis, and propose a prioritized remediation plan with measurable goals and a 90-day roadmap.
Sample Answer
Direct answer
I'd treat the two symptoms, slower review and more defects, as possibly the same root cause or two different ones, and not assume slower review caused more defects without checking. I'd generate hypotheses across people, process, and tooling, collect data to confirm or rule out each before proposing fixes, then sequence remediation by evidence strength and effort over a 90-day plan with explicit target metrics.
Structured elaboration
Hypotheses by category
People: the team grew or turned over, meaning fewer experienced reviewers are available, data to collect: headcount and tenure over the quarter, reviewer roster size over time. Or reviewer burnout, a few people doing most reviews, now overloaded, data to collect: review-count distribution per person, week over week.
Process: pull request (PR, a proposed code change submitted for review) size grew, bigger diffs take longer to review and hide more defects, data to collect: median and 90th-percentile PR size trend over the quarter. Or review requirements changed, for example a new mandatory second-reviewer rule added friction without adding value, data to collect: a policy change log cross-referenced against the timeline. Or priorities shifted, a launch or incident pulled focus away from review discipline, data to collect: a calendar of major initiatives and incidents in the quarter.
Tooling: continuous integration (CI, the automated build/test pipeline) got slower or flakier, so PRs sit longer waiting for a green build before a human even looks, data to collect: CI run duration and retry rate over the quarter. Or a tooling change silently reduced visibility of pending reviews, for example a broken notification bot, data to collect: a tooling change log and notification-delivery logs.
Data to collect, overall
- PR metadata over the quarter: size, time-to-first-review, time-to-merge, reviewer identity, per week
- Defect data: which PRs the post-release defects trace back to, and whether those PRs had unusually fast or slow review, small or large diffs, few or many comments
- Team roster and calendar: headcount changes, major initiatives, incidents, holidays
- Tooling telemetry: CI duration, notification delivery, any process or tooling changes deployed during the quarter
Prioritized remediation plan
- Address whichever hypothesis the data most strongly supports first, not the most dramatic-sounding one
- Prefer high-confidence, low-effort fixes before big structural changes; if the data shows PR size doubled, a size-nudge bot is cheap and directly targets the mechanism, a full process overhaul isn't needed yet
- Set a measurable target for each fix tied back to the original two symptoms (time-to-first-review, defect-escape rate), not just whether the fix shipped
90-day roadmap (adjust once the data confirms the actual cause)
- Days 1-15: instrument and collect the data above; don't change process yet, since changing multiple things at once makes it impossible to tell what worked
- Days 15-30: analyze, identify the one or two hypotheses the data actually supports, present the findings to the team
- Days 30-60: implement the highest-confidence, lowest-effort fix (for example, a PR-size nudge or reviewer load rebalancing) and hold everything else constant to isolate its effect
- Days 60-90: measure against the target (for example, time-to-first-review back under 24 hours, defect-escape rate back to baseline); if the metric hasn't moved, that hypothesis is disconfirmed and the next-highest-confidence one gets tried next
Worked example
Data collection shows: median PR size grew from 180 to 460 lines over the quarter, coinciding with a migration project that added several large, unavoidably broad refactor PRs; reviewer headcount and roster stayed flat; CI duration was stable. Defect data shows the doubled defects cluster specifically in the migration-related large PRs, not evenly across all PRs. This rules out the "reviewer burnout" and "tooling" hypotheses, since neither metric moved, and supports the "PR size" hypothesis instead. The remediation is scoped narrowly: a required PR-size-and-scope check specifically for the migration project (a splitting rule enforced via a bot comment plus a team norm), with a 60-day target of median PR size back under 250 lines and time-to-first-review back under 24 hours. Reviewer roster and CI tooling are left unchanged since the data didn't implicate them, avoiding a broad reorg that wouldn't have addressed the actual cause.
Trade-offs and pitfalls
- The single biggest mistake here is assuming causation, that slower review caused more defects, without checking whether both are downstream of a third cause like PR size; always check whether the defects actually trace back to the slow-reviewed PRs specifically, not just correlate in time
- Changing multiple things at once (new process AND new tooling AND reviewer reassignment) in the same 90 days makes it impossible to attribute the outcome to any one fix
- A remediation plan built before the data is in is a guess dressed up as a plan; resist the pressure to "do something now" before the first 15-day data-collection window closes
- Metrics chosen for the roadmap need a pre-quarter baseline to compare against; without one, "back to normal" isn't measurable
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
Design a custom lint rule (no implementation required) that enforces usage of a secure random function for token generation instead of non-cryptographic RNGs in a JavaScript codebase. Specify the detection heuristic, examples of violations and allowed patterns, false-positive risks, and minimal unit tests you would write for the rule.
Sample Answer
Direct answer
Design this as a static-analysis rule that inspects a JavaScript file's abstract syntax tree (AST, a tree representation of parsed code that a tool can walk programmatically) for calls to known non-cryptographic random number generator (RNG) functions, and flags any of them unless the same call, or a small local wrapper around it, resolves to an approved cryptographically secure API. The goal is a narrow, mechanical check, not a general proof that every random value in the codebase is "secure enough" for its actual use.
Structured elaboration
Detection heuristic. Walk CallExpression (a function call, like Math.random()) and MemberExpression (a property or method access, like crypto.randomBytes) nodes in the AST. Flag direct calls to known non-crypto sources: Math.random(), calls into common non-crypto libraries such as seedrandom or lodash's _.random(). Only treat a value as safe if it comes from an approved secure API: in Node.js, crypto.randomBytes() or crypto.randomInt(); in a browser or in Node's Web Crypto API, crypto.getRandomValues(). To reduce false positives, do a shallow, one-hop local resolution: if a flagged call happens inside a locally-defined function whose own body calls one of the approved secure APIs, treat call sites of that local function as safe too.
Examples of violations:
const token = Math.random().toString(36).slice(2);
const rng = require('seedrandom')();
const t = rng().toString();
const id = _.random(0, Number.MAX_SAFE_INTEGER).toString(36);
Allowed patterns:
const buf = crypto.randomBytes(16).toString('hex');
const arr = crypto.getRandomValues(new Uint8Array(16));
// local wrapper that itself calls a secure API: allowed via one-hop resolution
function secureToken(n) {
return crypto.randomBytes(n).toString('hex');
}
False-positive risks and mitigations.
- A locally-defined function literally named
randomthat internally calls a secure API would be flagged by a naive name-based check; the one-hop resolution above avoids that by checking what the function's body actually calls, not just its name. - Test files intentionally using
Math.random()for fixture data aren't a real security issue; mitigate with a path-based exemption for files under atest/or*.spec.jspattern, plus an explicit inline disable comment for anything the rule can't infer. - A third-party wrapper library the rule doesn't know about would be a false positive; mitigate with a small, team-maintained allowlist of module names that are known to wrap a secure API internally.
Minimal unit tests for the rule itself (each asserts a violation is or isn't reported):
- Violation:
const t = Math.random(); - Violation:
const seedrandom = require('seedrandom'); const t = seedrandom()(); - Allowed:
const { randomBytes } = require('crypto'); const t = randomBytes(16).toString('hex'); - Allowed:
const a = window.crypto.getRandomValues(new Uint8Array(8)); - False-positive mitigation, allowed via one-hop resolution:
function r(){ return require('crypto').randomBytes(8); } const t = r(); - Test-file exemption, allowed when the rule is configured to ignore test files:
// file: foo.test.jsfollowed byconst t = Math.random();
Worked example
Applying the rule to a real review scenario: a PR (pull request) adds const sessionToken = Math.random().toString(36); for a password-reset token. The rule fires on the Math.random() call, since it's a direct, unresolved call to a known non-crypto source with no secure wrapper anywhere nearby. The suggested fix in the rule's own error message points at the approved replacement directly: "Use crypto.randomBytes(n).toString('hex') instead of Math.random() for anything used as a token, session id, or password-reset code." Contrast this with a display-only, non-security "sample ID" generator using Math.random(), which is a legitimate use the rule should still flag by default (since the rule can't tell the two apart from syntax alone) but which the codebase can exempt with an inline disable comment naming why it's safe.
Trade-offs and pitfalls
A rule that's too aggressive creates noisy false positives that erode trust in the linter and get bulk-suppressed with a blanket disable comment, which defeats the purpose. A rule that's too narrow misses real violations hidden behind a wrapper function or import alias the rule doesn't recognize. Because a static rule is a heuristic, not a proof, it's worth pairing it with a short code-review checklist item ("is this token security-sensitive, and if so, does it come from a crypto-secure source") for anything the rule can't statically resolve, rather than treating a clean lint run as a guarantee.
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.