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.
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.
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.
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
You are reviewing a teammate's pull request that adds memoization to a function. What checks would you perform in code review to ensure correctness, memory safety, and thread-safety? Provide concrete review comments you might leave.
Sample Answer
Direct answer
Reviewing a memoization pull request (PR) means checking three separate things, not just "does it look right": is the cache key actually correct, is the cache's memory use bounded, and is it safe if the function can be called from more than one thread at once. Leave specific, line-referenced comments with a proposed fix, not a general "looks good."
Structured elaboration
Memoization means caching a function's return value keyed by its input, so a repeat call with the same input is served from the cache instead of recomputed.
Correctness
- Does the cache key capture every input that affects the output, including default/optional arguments and any instance state for a method? A key that's missing one input will return a stale, wrong answer for calls that only differ in that field.
- Is the function actually deterministic and side-effect-free for a given input? If it has a side effect (an increment, a log line, a downstream call), that side effect now only fires on a cache miss, which the PR should call out as an intentional, documented behavior change.
- Is there any invalidation path, or does a cached value live forever even after the underlying data it depends on changes?
Memory safety
- Is the cache bounded, for example an LRU (least-recently-used) cache with a maximum size, or a TTL (time-to-live) that expires old entries, or can it grow without limit and eventually cause an OOM (out-of-memory) failure on a long-running process?
- If the cache keys or values hold references to large or long-lived objects (like an instance whose method is being memoized), does the cache keep those objects alive longer than intended? That's a slow memory leak, not a crash, so it's easy to miss in a quick review.
Thread-safety
- If two threads call the function with the same new argument at almost the same moment, what happens? A plain dict/HashMap being read and written by two threads without synchronization can corrupt its internal structure, not just return a stale value.
- Is the "check cache, then compute, then store" sequence atomic, or can two threads both miss the cache and both do the (possibly expensive) work at once? That's usually acceptable for a pure read-through cache, but it should be a stated decision, not an accident.
Worked example
Concrete review comments for this PR, each pointing at a specific problem and a fix:
- "Nit: please add a test that calls this function twice with identical inputs and asserts the second call returns the cached result without re-running the underlying computation (for example by mocking the expensive call and asserting it was invoked once)."
- "This cache is an unbounded dict, so a long-running process will keep every distinct input in memory forever. Can we bound it, for example with
functools.lru_cache(maxsize=...)in Python, or an explicit LRU wrapper in other languages?" - "This cache isn't synchronized. If this function can be called from more than one thread, either guard the check-then-store sequence with a lock, or use a data structure built for concurrent access (a
ConcurrentHashMapin Java, or a dict behind an explicit lock in Python)." - "Key correctness: the cache key here only includes
user_id, but the function's output also depends onas_of_date. Two calls for the same user on different dates will incorrectly return the same cached value."
Trade-offs and pitfalls
A lock around the whole cache is simple but serializes every call, even ones for different keys; a per-key lock or a genuinely concurrent map avoids that contention at the cost of more complex code, worth it once the function is actually called concurrently at meaningful volume, not before. An unbounded cache is the fastest option and the easiest to get wrong in production, since it fails silently (memory just grows) until it doesn't. If the cached value is a mutable object, a caller who mutates what they got back can corrupt the cached copy for every future caller unless the function returns a defensive copy, that's a subtle correctness bug worth its own comment when the return type is mutable.
List five elements you always include in a pull request description to make reviews efficient for team members and new joiners. Explain why each element matters for maintainability, discovery, and onboarding.
Sample Answer
Direct answer
A good pull request (PR, a proposed code change submitted for review) description answers what changed, why, how it was tested, what the reviewer should focus on, and what's deliberately out of scope. Each element saves a reviewer from reconstructing context from the diff alone, and later saves an engineer doing archaeology on why the code looks the way it does.
Structured elaboration
Five elements and why each matters
- What changed (a one-to-two sentence summary): gives the reviewer a frame before reading the diff line by line; without it they infer intent from code, which is slower and more error-prone
- Why (the motivating problem, with a linked ticket or incident if there is one): this is the piece that survives long after the code changes again; an engineer later using
git blameor the commit log to understand "why is this here" finds the why in the description, not the diff - How it was tested: tells the reviewer what confidence already exists (unit tests added, manually verified in staging), so they know what's already covered versus what still needs scrutiny
- What to focus review on: the author usually knows which part of the diff is riskiest or most novel; naming it directs a time-constrained reviewer's attention where it matters most
- What's explicitly out of scope: prevents scope-creep comments ("why didn't you also fix X") and tells a future reader that a related-looking issue was a deliberate non-goal, not an oversight
Why this matters beyond the immediate review
- Maintainability: the description becomes permanent documentation attached to the commit history; months later it's often the only record of intent
- Discovery: someone tracing "why does this function do X" through the commit log finds the description as the answer, faster than re-deriving it from the diff
- Onboarding: a new engineer reading recent PR history to understand a codebase's evolution gets a narrated version of the system's history instead of bare diffs
Worked example
A PR titled "Fix race condition in session refresh" with the description "fixes bug, see diff" forces the reviewer to reverse-engineer what race condition, why this fix addresses it, and whether it was actually reproduced. Contrast: "What: adds a lock around session-token refresh. Why: intermittent authentication failures under concurrent requests, traced to two requests refreshing the same token at once and one overwriting the other's write. Tested: added a test that fires 50 concurrent refresh calls and asserts exactly one network call happens; manually reproduced the original bug on staging before the fix and confirmed it no longer reproduces after. Focus review on: the lock's scope, it needs to cover the full read-modify-write, not just the write. Out of scope: the underlying token storage isn't thread-safe either, that's a separate, larger fix tracked elsewhere." A reviewer can now scrutinize exactly the risky part (the lock's scope) instead of re-deriving the whole problem, and anyone reading this PR later understands both the bug and the deliberate boundary around the fix.
Trade-offs and pitfalls
- A template that's too long or bureaucratic gets filled in with boilerplate nobody reads; keep it to the elements that actually change reviewer behavior
- A "why" section that just restates the ticket title without the actual reasoning doesn't help; the value is in the reasoning, not a ticket number
- Descriptions get stale if the PR's scope changes mid-review and nobody updates them; treat the description as something to revisit before merge, not only before opening the PR
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.