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.
How would you detect secrets leaked in a PR or git history, and what would you actually do about it: immediate reviewer actions, secret rotation, and cleaning up the history? Name the tools you'd reach for and walk through the trade-offs of your remediation approach.
Sample Answer
Direct answer
I detect leaked secrets with automated scanning, both in CI on every PR and as a pre-commit hook on developer machines, using a dedicated secret scanner rather than relying on human review to spot them. Once found, the sequence is always the same regardless of remediation approach: rotate the credential immediately, then separately decide whether the exposure also needs to be scrubbed from git history, since those are two different problems with two different urgencies.
Structured elaboration
Detection tools. gitleaks and truffleHog scan a repository or a diff for patterns that look like credentials (API key formats, private key headers, common token shapes); git-secrets is a lighter pre-commit-focused option. Many hosted platforms also offer built-in secret scanning on push, which can block the push entirely before the secret ever reaches a shared branch, which is strictly better than catching it after the fact in review.
Immediate reviewer actions. If it's caught in a PR that hasn't merged yet: block the PR, and if the branch hasn't been pushed to a shared/protected branch, the author can often just rewrite their local history (amend or interactive rebase) and force-push a clean version, meaning the secret may never actually land in the repository's permanent history at all. Either way, treat the credential as compromised the moment it left a developer's machine, since it may already be visible in CI logs, in anyone who pulled the branch, or cached by the hosting platform itself, regardless of whether it ever reaches the default branch.
Secret rotation. Rotate first, always, independent of whatever happens to git history: generate a new credential, update every system that consumes it (deployment pipeline, running services, other developers' local environments), verify the new one works, then revoke the old one. This is the step that actually closes the exposure; history cleanup on its own does not, since a secret already scraped or cloned by someone stays valid until it's rotated.
Cleaning git history. Only needed if the secret already reached a shared branch. git filter-repo is the currently recommended tool for rewriting history (it replaced the older, simpler BFG Repo Cleaner as the generally recommended option, though BFG is still common and simpler for basic cases). Both require a force-push and coordination: every existing clone and open PR based on the old history becomes stale and needs to be re-cloned or rebased, which is real organizational disruption, not just a technical step.
Worked example
An AWS access key literal shows up in config.py in an open PR. As reviewer, I block the merge immediately and comment describing exactly what to do: remove the key from the diff, and separately (in parallel, not instead of) notify whoever owns that AWS account to rotate the key right now, since the moment it was pushed to the PR's remote branch it was visible in GitHub's systems and CI logs regardless of whether the PR ever merges. Because the PR hasn't merged to the default branch, the author can amend the offending commit locally and force-push the corrected branch, meaning the key likely never needs a full history rewrite of the shared branch, only its own rotation.
Trade-offs and pitfalls
Rewriting shared git history is disruptive enough (a mandatory force-push, every collaborator needing to reset their local clone) that many teams choose to rotate-and-leave-history-alone rather than rewrite, accepting that the old, now-revoked value stays visible in history forever; that's a reasonable trade when the credential can be fully rotated, and a much weaker option when the "secret" is something that can't be rotated, like a hardcoded document containing real customer data. The most common pitfall is treating history rewriting as the fix and skipping or delaying rotation, when rotation is the step that actually stops the credential from being usable, and history cleanup by itself does nothing about a secret that was already copied somewhere before it was removed.
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.
When reviewing infrastructure code, how do you evaluate what tests are appropriate? Describe a testing strategy (unit, integration, end-to-end) for a Terraform module that provisions a VPC, subnets, and an autoscaling group used by several services. Explain what each test layer validates and how you'd run them safely in CI.
Sample Answer
Direct answer
I pick the test layer by how expensive and how real it needs to be to catch the risk: fast, free static checks for every PR, real-but-throwaway cloud resources for anything that has to prove the infrastructure actually works, and a full end-to-end pass sparingly, since it's the slowest and most expensive layer. For a module provisioning a VPC (virtual private cloud, an isolated network), subnets, and an Auto Scaling group (ASG, a group that automatically adds or removes instances to match demand), all three layers earn their place.
Structured elaboration
Unit-style / static tests. Tools: terraform validate for syntax, tflint for provider-specific correctness and style, and a policy-as-code tool (Checkov or Open Policy Agent) for security and convention rules. What they validate: the configuration is syntactically valid, uses provider arguments correctly, follows naming conventions, and doesn't violate a known policy (a publicly-open security group, a missing required tag). How to run safely: on every PR, no cloud resources touched, so it's fast and free to run as often as needed.
Integration tests. Tools: Terratest (a Go testing library for Terraform) or a similar framework that can actually run terraform apply, inspect the result, then terraform destroy. What they validate: that the module actually creates what it claims to, for example that subnet CIDR blocks (Classless Inter-Domain Routing blocks, the notation for an IP address range like 10.0.0.0/24) are correctly sized and non-overlapping, that subnets land in the intended availability zones, and that the ASG's launch configuration references a valid, existing image. How to run safely: against an isolated, short-lived sandbox account or project, using scoped, least-privilege credentials, with every resource tagged for the test run and torn down automatically, including on failure, so a crashed test doesn't leave orphaned billable resources behind.
End-to-end tests. What they validate: that the pieces actually work together as a live network, for example that an instance launched by the ASG in a private subnet can reach the internet through a NAT gateway, or that a load balancer in the public subnet can actually route to instances in the private one. How to run safely: in a dedicated, ephemeral environment, run less frequently (on merge to the main branch, or nightly) rather than on every PR, since it's the slowest and most expensive layer, with the same automatic teardown and budget guardrails as integration tests.
Worked example
The module provisions a VPC with CIDR block 10.0.0.0/16, split into three public subnets across three availability zones: 10.0.0.0/24, 10.0.1.0/24, and 10.0.2.0/24. Each of those is a distinct, non-overlapping /24 (256 addresses) carved consecutively out of the /16. A unit-style check confirms the CIDR math is valid and non-overlapping without touching any cloud account at all. An integration test actually applies the module in a sandbox account and asserts, via the cloud provider's API, that three subnets exist, each in a different availability zone, each with the expected CIDR. An end-to-end test then launches a real instance through the module's Auto Scaling group and confirms it can reach an external endpoint, proving the NAT gateway and routing are actually wired correctly, not just declared correctly.
Trade-offs and pitfalls
Running integration and end-to-end tests on every PR would catch problems faster but at real cost and risk: real cloud resources cost money even torn down immediately, and a bug in the teardown logic itself can leave orphaned resources running indefinitely if there's no separate cleanup safety net. The most common pitfall is skipping the unit-style layer because it "doesn't test anything real," when in practice it's what catches the majority of simple mistakes (a bad CIDR, a disallowed instance type) before they ever cost a cloud API call, leaving the expensive layers to catch the smaller number of problems that only show up when resources actually exist.
Leadership: You're in a large engineering org where reviewers are overloaded and PR latency is high. Propose a scalable manual-review strategy combining automation, triage, reviewer assignment rules, and code ownership. Explain how to maintain quality while reducing time-to-merge and preventing reviewer burnout.
Sample Answer
Direct answer
At org scale, "review faster" doesn't work, the fix is redesigning how review load is distributed: automate everything mechanical before a human ever opens a diff, route each pull request (PR, a proposed code change submitted for review) to the right owner instead of a free-for-all queue, triage by risk so low-risk changes take a lightweight path, and make reviewer load a metric leadership actually watches, the same way they'd watch on-call load.
Structured elaboration
Automation removes load before a human sees the diff
- Continuous integration (CI, the automated build/test pipeline) blocks on lint, type checks, unit tests, and coverage delta, so reviewers never comment on things a machine already caught
- A bot flags PR size and touched-file risk (for example, "this modifies auth middleware") so a reviewer knows what they're walking into before opening it
Triage by risk
- Classify changes into tiers: low-risk (docs, config, isolated feature-flagged code) gets a single reviewer or even auto-merge after CI passes; medium risk gets normal review; high-risk (shared libraries, security, data migrations, billing) gets a mandatory named owner plus a second reviewer
- This concentrates scarce senior reviewer attention on what actually needs it instead of spreading it thin and evenly
Reviewer assignment and code ownership
- CODEOWNERS-style routing (a config file mapping directories to the team or person who owns that code) auto-assigns based on what the PR actually touches, instead of a random queue
- Load-balance assignment within an ownership group (round robin or least-loaded) instead of everyone requesting the one person known to be thorough
- Rotate a weekly "on-call reviewer" role per team so load doesn't permanently concentrate on the same two or three people
Preventing burnout while cutting time-to-merge
- Cap how many PRs a single reviewer is expected to have open at once, visible on a shared dashboard
- Protect a daily review block instead of letting review compete with deep work as constant interrupts
- Track reviewer load as a first-class metric leadership actually looks at, not an afterthought
Worked example
An org of 300 engineers has PR latency (open to merge) at a median (the middle value, half of PRs merge faster and half slower) of 3 days, driven by 15% of reviewers absorbing 60% of review volume, the people everyone requests because they're known to be thorough. The plan: introduce CODEOWNERS-based auto-assignment for the 20 highest-traffic directories, add a bot that classifies PRs into risk tiers from touched paths and diff size, and set a rule that low-risk PRs need one reviewer with a 4-business-hour service-level agreement (SLA, an explicit response-time target) while high-risk PRs need a named owner with a 1-business-day SLA. Leadership adds a dashboard showing open-review-count per person, and anyone over a threshold (say, 8 open reviews) gets rebalanced by their lead. After a quarter, median latency for low-risk PRs drops sharply because they no longer queue behind high-risk items, and concentration on the top reviewers eases because ownership-based routing spreads assignments across each team instead of funneling everything to a few known-good individuals.
Trade-offs and pitfalls
- Automated risk-tiering can misjudge risk (a one-line change to a rate limit is "small" but dangerous), keep a human override on the classification
- CODEOWNERS routing can create silos where only the "owner" ever reviews a path, losing the cross-pollination review normally provides; rotate ownership periodically
- Cutting time-to-merge by lowering the bar (fewer required reviewers everywhere) trades quality for speed; the real lever is distributing existing rigor better, not removing it
- A reviewer-load dashboard used punitively instead of for rebalancing damages trust fast; be explicit it's a load-balancing signal, not a performance metric
A PR adds a GitHub Actions workflow that builds artifacts and deploys to production. Review the YAML below and identify security, caching, and reliability issues. Suggest concrete fixes.
name: deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: echo $SECRET_KEY
- run: curl -sL https://example.com/install.sh | bash
- run: ./deploy.sh
What would you change and why?
Sample Answer
Direct answer
This workflow has real problems in all three categories asked about. Security: it echoes a secret to the build log and pipes a remote install script straight into bash with no verification, and it deploys to production on every single push with no approval gate. Caching: nothing is cached, so every run rebuilds from scratch. Reliability: there is no build or test step before deploy, no pinned action or tool versions, and no rollback path if deploy.sh fails partway through.
Structured elaboration
Security
echo $SECRET_KEYprints the secret's value straight into the build log, which anyone with log-read access can see, and the workflow never even setsSECRET_KEYfromsecrets.SECRET_KEYin the first place, so as written this line is also just dead, misleading code.curl -sL https://example.com/install.sh | bashruns unaudited code from a remote server directly as root-equivalent on the runner: a supply-chain compromise of that URL becomes a compromise of the deploy job. Download the script, verify a checksum or signature against a value you pin yourself, then run it.actions/checkout@v2is pinned to a mutable major-version tag; a compromised or hijacked tag could silently change what code checks out. Pin to a current major version (v7 as of this writing) or, for maximum supply-chain safety, a full commit SHA.- The job has no
permissionsblock, so it inherits broad default token permissions it likely doesn't need. Long-lived cloud credentials insecretscan usually be replaced entirely with OpenID Connect (OIDC, an identity-token exchange protocol): the job requests a short-lived, workflow-scoped token from the cloud provider instead of storing a static key.
Caching
- Nothing here is cached, so dependency installs and any build step re-run from a cold cache on every push.
actions/cache, keyed off a lockfile hash, is the standard fix for anything with reusable dependency state.
Reliability
on: [push]with no branch filter means every push to every branch attempts a production deploy. Restrict the trigger to the deploy branch, and consider requiringworkflow_dispatch(a manual trigger button) for production specifically.- There is no build, test, or lint step before
deploy.shruns, so a broken commit deploys straight to production. - No
environment:protection rule, so there is no required approval, no audit trail, and secrets are available to every job rather than scoped to a protected environment. - No timeout, no health check after deploy, and no visible rollback strategy if
deploy.shfails halfway through.
Worked example
A corrected version of the workflow, addressing every issue above:
name: deploy
on:
push:
branches: [main]
permissions:
id-token: write # enables OIDC token exchange with the cloud provider
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # requires a configured approver, scopes secrets to this job
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Cache dependencies
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-deps-${{ hashFiles('requirements.txt') }}
- name: Run tests
run: ./run_tests.sh
- name: Install deploy tool (pinned + verified, not piped blind)
run: |
curl -sSLO https://example.com/install.sh
echo "<pin-the-real-published-checksum-here> install.sh" | sha256sum -c -
bash install.sh
- name: Deploy
run: ./deploy.sh
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }} # bound only where used, never echoed
Trade-offs and pitfalls
Requiring an environment approval and a test step before deploy adds real friction and latency to every deploy, which is a deliberate trade against the speed of the original one-shot script; for a low-risk internal tool that trade might not be worth it, but for anything customer-facing it almost always is. Pinning actions to a full commit SHA is the strongest supply-chain guarantee but adds maintenance overhead, since you have to manually bump the SHA to pick up fixes; pinning to a major version tag like @v4 is the common middle ground. A subtle pitfall: even secrets referenced correctly via ${{ secrets.X }} get automatic log masking from GitHub, but that masking is best-effort, exact-string matching that a transformed or partially-printed value can defeat, so "never print a secret at all" stays the real rule rather than relying on masking as a safety net.
Unlock Full Question Bank
Get access to all 31 Code Review and Working with Existing Codebases interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.