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.
During a code review the author and reviewer disagree on an API naming decision that affects many callers. Describe a step-by-step conflict resolution you would follow as the reviewer to reach a pragmatic decision while preserving team trust, including whether to escalate, prototype alternatives, or perform a quick user impact analysis.
Sample Answer
Direct answer
Don't let the disagreement stay abstract: get concrete about what's actually at stake, how many callers exist and how costly a rename would be later, state your reasoning once clearly, and if it's still unresolved, use a fast, low-cost tiebreaker (a quick prototype, a third opinion, or an explicit decision-maker) rather than letting the thread drag on.
Structured elaboration
- Separate preference from cost. Ask explicitly whether this is reversible. An API used by many callers is expensive to rename later, a breaking change or a long deprecation, so the bar for insisting on a preferred name should scale with how many callers exist and how costly a change would be, not with how strongly either person feels.
- State your reasoning once, concretely, and ask for theirs. Name the specific downside, not just "I don't like this name": "
getUserreads as a lookup that returns null, but this always creates one if missing, that's surprising for the many call sites that assume a pure read." - If still unresolved, do a quick, cheap check instead of more debate. Grep the existing callers to see how the API is actually used, or spend ten minutes prototyping both names in real call sites to see which reads better. This replaces opinion with evidence.
- If still stuck, get a third opinion fast. A tech lead, or whoever owns that part of the API, makes the call; anyone involved can request this, and it's cheap to ask compared to letting the PR (pull request) stall.
- Once decided, commit and close the thread visibly. Whoever didn't get their preference says so out loud ("okay, going with X") so the disagreement doesn't linger as an unresolved grudge, and the reasoning gets written down briefly (in the PR or a short doc) so the next naming debate doesn't restart from zero.
Worked example
Two engineers disagree on getUser versus findOrCreateUser for a method with roughly forty existing call sites. The reviewer greps the callers and finds most assume a pure lookup with no side effects. Bringing that evidence back, "most callers don't expect this to write anything, the name should signal that," the author agrees the evidence changes the calculus, and they land on findOrCreateUser. A short note is added to the PR explaining the reasoning so a future reader understands why the name is what it is.
Trade-offs and pitfalls
Escalating to a tech lead for every minor naming choice trains the team to stop deciding things themselves. Conversely, letting every naming disagreement burn hours of back-and-forth stalls delivery on something that's often genuinely reversible later behind an alias. The failure mode to avoid is a reviewer pulling rank, "just do it my way, I'm the reviewer," instead of showing the reasoning; that resolves the immediate PR but erodes trust for the next one.
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.
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.
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
You are reviewing a data migration that renames a heavily used column and requires backfilling millions of rows. Design a rollback-safe migration strategy that can be reviewed and approved. Cover schema changes, dual-write/read strategies, backfills, verification, monitoring, and how code review should verify each migration step.
Sample Answer
Direct answer
A rollback-safe rename plus backfill never touches the old column or existing readers directly. It adds the new column alongside the old one, writes to both while backfilling the new one in batches, verifies the backfilled data matches, only then switches reads over behind a flag, and keeps the old column around for a retention window so any step can be reversed just by flipping the flag back, not by undoing a destructive change.
Structured elaboration
Each phase below names what code review should specifically confirm before approving it, as a "Review check," plus what to monitor once it ships.
1. Schema change. Add the new column as nullable, with no constraints yet:
ALTER TABLE events ADD COLUMN new_name text NULL;
Review check: confirm this specific statement is additive only and backward-compatible, meaning every existing reader and writer keeps working unmodified the moment this ships, with zero application changes required yet.
2. Dual-write. Deploy an application change, behind a feature flag, that writes both the old and new column on every write to a row. Review check: is the write to both columns transactional or otherwise guaranteed consistent (not "write old, then separately and non-atomically write new"), and is the flag off by default so this ships dormant before anything depends on it?
3. Backfill. A batched, idempotent job fills in the new column for existing rows, only where it's still NULL, ordered by primary key, with a checkpoint so it can resume after an interruption instead of restarting from row one:
UPDATE events
SET new_name = old_name
WHERE id BETWEEN :batch_start AND :batch_end
AND new_name IS NULL;
Review check: is progress persisted somewhere durable (not just in the running process's memory), and does re-running an already-completed batch do nothing (true idempotence), not create incorrect data? Monitoring: track batches completed, rows backfilled, replication lag, and write error rate on the table for the duration of the backfill, and pause automatically if replication lag crosses an agreed threshold.
4. Verification. Before trusting the backfill, sample a random set of rows and confirm new_name matches what old_name implies for each. Review check: is the sample size and comparison method actually specified in the PR, not just asserted as "we verified it"?
5. Read cutover. Only after verification passes, flip the flag so reads prefer the new column, falling back to the old one if the new one is somehow still empty for a given row. Review check: is the fallback logic actually tested, not just written? Monitoring: application error rate and query latency on this table specifically, right after the flag flips, since a regression here is the trigger for the rollback shown in the diagram below.
6. Cleanup. Once reads have run on the new column successfully for a defined retention window, make it NOT NULL, add any index it needs, and only then drop the old column, in a separate, later PR. Review check: is dropping the old column genuinely a separate step from everything above, so it can never accidentally ship bundled with a change that hasn't been verified yet?
flowchart LR
A[Step 1: add new_name column, nullable] --> B[Step 2: dual write old_name plus new_name]
B --> C[Step 3: backfill new_name in batches where NULL]
C --> D{Verification: sampled row values match}
D -- mismatch found --> C
D -- fully verified --> E[Step 4: flip reads to new_name behind a flag]
E --> F{Error rate normal after cutover}
F -- regression --> G[Rollback: flip flag back to old_name, dual write stays intact]
F -- healthy --> H[Step 5: make new_name NOT NULL, add index concurrently]
H --> I[Step 6: drop old_name after a retention window]
Worked example
Renaming user_email to primary_email on a table with 40 million rows, backfilled in batches of 5,000 rows: that's 40,000,000 / 5,000 = 8,000 batches total. With a short pause between batches to keep replication lag bounded, the job runs as a background process over however long it takes to work through all 8,000 batches, checkpointing its position after each one so a restart resumes from the last completed batch instead of row one. Verification samples 10,000 random rows after the backfill reports complete and confirms primary_email equals user_email for every one of them before the flag is ever flipped to prefer reads from the new column.
Trade-offs and pitfalls
Every step here is reversible specifically because the old column and old read path stay intact until the very last, separate cleanup step, which is exactly what makes this slower and more code than a single rename statement; that trade is worth it for a heavily-used column and wrong for a rarely-touched internal table, where a single migration with a maintenance window might be simpler and perfectly safe. The most dangerous version of this pattern to review is one that quietly combines two of these steps, most often shipping the read cutover and the old-column drop in the same change, which collapses the rollback safety the whole design exists to provide.
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.