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 a systems engineer reviewing infrastructure code (Terraform, Ansible, Bash, Python), create a practical code-review checklist you would apply to pull requests. The checklist should cover correctness, clarity, maintainability, performance, security, testability, operational readiness (observability, rollback), documentation, and dependency/secret handling. For each checklist item include a 1-2 sentence rationale and a concrete example of what to look for in PR diffs or code.
Sample Answer
Direct answer
Infrastructure code (Terraform, Ansible, Bash, and Python scripts that provision or configure systems) gets reviewed against the same bar as application code, plus more weight on blast radius (how much of the system, or how many users, a bug here could damage before anyone catches it), secrets, and rollback, because a bug here can take down a live environment or leak a credential, and there's rarely a staging replica that catches it first.
Structured elaboration
| Category | Why it matters | What to look for in the diff |
|---|---|---|
| Correctness | A terraform plan shows exactly what will change before it runs, and it should be read, not trusted blindly | Whether a plan for a security-group rule shows an in-place update or a destroy-and-recreate; the latter causes an outage window most authors don't intend |
| Clarity | Infra code gets read under pressure, during an incident, far more than application code | A count loop indexed by a bare x is worth flagging in favor of a named for_each map, so an on-call engineer can map a resource back to its purpose quickly |
| Maintainability | Infra changes accumulate as copy-pasted modules over time | Three near-identical resource blocks for three environments is a sign this should be parameterized, or the next edit will update one copy and miss the others |
| Performance | A slow provisioning script turns a fast deploy into a slow one for everyone using it | An Ansible playbook issuing one API call per host across 200 hosts instead of a single batched call |
| Security | Infra code decides where credentials live and what's network-exposed | A security group opened to 0.0.0.0/0 on a database port instead of the actual caller's CIDR range (a way of writing a block of IP addresses, e.g. 0.0.0.0/0 means every address on the internet) |
| Testability | Infra is hard to unit test, so review for whether it can be validated before touching production | terraform plan/terraform validate in CI (continuous integration), a linter (tflint, ansible-lint), or a dry-run flag on a script |
| Operational readiness | Every change needs a way to tell if it worked and a way to undo it | Does the PR description name a specific health check or metric to watch after the apply, and the exact rollback command |
| Documentation | Infra decisions get lost fastest since nobody revisits them until something breaks | A non-obvious lifecycle { ignore_changes } block (a Terraform setting that tells it to stop tracking changes to a specific field after the resource is created) should carry a one-line comment explaining why, or the next engineer will "fix" it and reintroduce the original bug |
| Dependency/secret handling | Infra code references pinned versions and, more dangerously, credentials | A hardcoded API key or password in a .tfvars file or Bash script should block the PR outright; it belongs in a secret manager, and an unpinned module version like >= 1.0 should be flagged since it can silently pull in a breaking change |
Worked example
A PR adds a Terraform module that provisions an S3 bucket for application logs. Correctness: the plan shows a new bucket being created, no destroy. Security: the bucket policy is checked for public read access left open by default, flagged and narrowed. Testability: terraform validate and a linter both run in CI. Operational readiness: the PR description names the CloudWatch metric to check post-apply and states the rollback is terraform destroy on this specific module since nothing else depends on it yet. Documentation: a comment explains why a 90-day lifecycle expiration was chosen. Secret handling: no credentials appear in the diff; the module references an existing secret manager entry.
Trade-offs and pitfalls
Applying every item on this checklist to a one-line change to a dev-only resource is overkill; scale depth to blast radius, not to the existence of the checklist. The most common wrong turn is approving because the Terraform plan "looks clean" without actually reading whether a resource will be destroyed and recreated, which is the single most frequent way an infra review misses an avoidable outage.
You are reviewing automation code that performs TLS certificate rotation for internal services. Identify failure modes, security checks, and test cases you would require. Propose a robust design for rotation that avoids downtime, supports emergency rollback, and ensures private key secrecy during rotation.
Sample Answer
Direct answer
Review this as three linked concerns: what can go wrong (failure modes), what has to always be true for security (checks), and what has to be provably tested before this runs against real services. Design the rotation itself around a staged, node-by-node rollout that avoids downtime, keep the old certificate valid in parallel until every node is confirmed on the new one, support a fast, tested rollback, and keep private key material out of the automation's own logs, disk, and version control at every step.
Structured elaboration
TLS (Transport Layer Security) is the protocol that encrypts and authenticates network connections using a certificate and a private key.
Failure modes. Deploying an expired or not-yet-valid certificate, often from clock drift between the automation and the certificate authority. A partial rollout that leaves some instances on the old certificate and some on the new one, so clients see inconsistent trust depending on which instance they hit. A crash mid-rotation that leaves private key material readable on disk longer than intended. A failed service reload that causes real downtime instead of a clean handoff. Rolling back to a certificate whose private key may itself be the thing that's compromised, which isn't automatically safe.
Security checks. Private keys should be generated inside, and never leave, a KMS (key management service) or HSM (hardware security module, a dedicated device that generates and stores keys so the automation software never handles the raw key bytes). Every rotation run should validate the new certificate's chain, its expiry, and that its subject or SAN (subject alternative name, the field listing which hostnames the certificate is valid for) actually matches the service, before that certificate is deployed anywhere. Access to trigger a rotation should be restricted, and every rotation should be logged for audit.
Test cases. Unit tests for certificate parsing and validation logic. An integration test running the full pipeline against a staging certificate authority. A chaos test that kills a node mid-rollout and confirms the system recovers to a consistent state. Explicit negative tests for an expired, malformed, or revoked certificate, confirming each is rejected rather than silently deployed. A rollback test that intentionally deploys a bad certificate and confirms the automated rollback actually restores service.
Worked example
A concrete rollout design for a fleet of internal services behind a load balancer: generate the new key and certificate inside the KMS/HSM, so the automation only ever handles a reference or token, never raw key bytes. Validate the new certificate, chain, expiry, and SAN match, before touching any node. Roll it out node by node: deploy to one node, health-check it with an actual mutual-TLS (mTLS, TLS where both sides present a certificate) probe against that specific node, and only then move to the next. Keep the previous, still-valid certificate available in parallel throughout the rollout, so there's no window where a client can be rejected by both the old and the new certificate at once. If a health check fails partway through, stop and automatically roll the affected nodes back to the last-known-good certificate. Only after every node is confirmed on the new certificate does the automation retire the old one.
Trade-offs and pitfalls
A node-by-node rollout with health checks is slower than an all-at-once swap, but avoiding a fleet-wide outage from one bad certificate is the entire point of the design. Keeping the old certificate valid in parallel during rollout is the actual downtime-avoidance mechanism, and revoking it too early is the single most common way teams reintroduce the exact outage this design exists to prevent. If the rollback target's own key is the thing suspected of being compromised, rolling back to it isn't actually safe, that scenario needs a fresh emergency issuance instead, so the runbook needs to clearly separate "bad rotation, roll back" from "compromised key, reissue" as two different playbooks rather than one.
You are reviewing a large refactor that touches core library internals. Outline a plan to split the change into incremental PRs that are safe to merge independently. Include a proposed sequence, how to keep the system working at each step, feature flagging, test strategies, and rollback points.
Sample Answer
Direct answer
I sequence the refactor so the public interface changes land first, additively, before internals move; I migrate call sites one at a time behind a feature flag with small commits that each leave the system green, and I pick a rollback point at every seam so the whole plan is expand-migrate-contract rather than one big merge.
Structured elaboration
Sequencing principle: expand, migrate, contract
- Expand: add the new internal implementation alongside the old one, without removing anything
- Migrate: switch call sites over incrementally, one at a time, behind a flag
- Contract: once every call site is migrated and validated in production, delete the old code path in its own final, low-risk pull request (PR, a proposed code change submitted for review)
Interface first
If the refactor changes a library's public interface, not just its internals, land the new interface signature first as a strictly additive change (a new method or overload alongside the old one) before implementation work even starts. This lets consumer teams see or start adopting the new shape while the internals are still being built, instead of waiting for one giant landing.
Verify against real consumers, not just the library's own tests
Before merging any internal change, run each consumer's recorded expectations of the library (its actual inputs and outputs, sometimes called a consumer-driven contract) against the new implementation. This catches the case where the internals pass the library's own unit tests but silently break an assumption a downstream consumer relied on that the library never tested itself.
Branching and commit order
Prefer small commits on a short-lived branch or trunk-based development, with the feature flag doing the isolation, over a long-lived branch. A stale long-lived branch reintroduces the exact big-bang-merge risk this plan exists to avoid. Order commits so each one compiles, passes CI (continuous integration, the automated build/test pipeline), and leaves the system deployable: add the new path unused, add the flag defaulting off, migrate the lowest-risk call site, validate, migrate the rest in ascending risk order, then remove the old path.
Test strategy at each step
- Characterization tests (tests that pin down what the old code currently does, written before touching it) on the existing implementation first
- Consumer contract tests run on every PR in the sequence, not only the last one
- Shadow-run the new path against the old path's output on real traffic where feasible, before flipping the flag for real
Rollback points
Every PR that flips a flag default is a rollback point: flipping the flag back is the fast rollback, no code revert or redeploy needed. The final "delete the old path" PR is the one true point of no return, so it ships last, only after a full soak period with the flag on.
flowchart LR
A["PR1: add new path, flag off"] --> B["PR2: add consumer contract tests"]
B --> C["PR3: implement new internals"]
C --> D["PR4: migrate lowest-risk consumer"]
D --> E["PR5-7: migrate remaining consumers"]
E --> F["PR8: delete old path"]
Worked example
Refactoring a core PaymentValidator library used by 4 internal services, changing its algorithm from a synchronous rule chain to a rule-table lookup for performance. PR1 adds validate_v2() alongside the existing validate(), unused, behind a payment_validator_v2 flag defaulting off, plus characterization tests pinning validate()'s current output on 50 representative inputs. PR2 adds contract tests recording each of the 4 consumers' actual usage patterns against validate(). PR3 implements validate_v2(), which must pass every contract test. PR4 migrates the lowest-risk consumer, an internal reporting service, behind its own flag, monitored for a week. PR5-7 migrate the remaining 3 consumers one at a time, with live payment processing last, each with its own soak period. PR8, once all 4 consumers are on v2 and soaked through a full billing cycle, deletes validate() and the flag. If PR5's shadow comparison shows a discrepancy in production, the rollback is flipping that one consumer's flag back, not reverting code.
Trade-offs and pitfalls
- Expand-migrate-contract takes longer in calendar time and leaves two code paths live temporarily, itself a maintenance cost and a place bugs can hide (fixing one path and forgetting the other)
- Skipping consumer contract tests and relying only on the library's own unit tests is the classic way this goes wrong: internals look correct in isolation but violate an assumption a consumer depended on
- A stale long-lived branch reintroduces the exact big-bang-merge risk the plan exists to avoid; if small trunk-based commits aren't feasible, that's a signal the split needs to be finer-grained, not a reason to accept one large final merge
- Deleting the old path before every consumer has soaked removes the rollback safety net that made the whole plan low-risk in the first place
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 are reviewing an Ansible playbook intended to be idempotent. Identify problems in this snippet and propose changes to make it idempotent and testable.
- hosts: web
tasks:
- name: install nginx
command: apt-get install -y nginx
- name: create conf
copy:
content: "server { listen 80; }"
dest: /etc/nginx/sites-enabled/default
- name: restart nginx
service:
name: nginx
state: restarted
What would you change and why? How would you test the playbook in CI?
Sample Answer
Direct answer
None of the three tasks here is idempotent, meaning running the playbook a second time against a server that's already correctly configured should report no changes, but this one still reports changes, or worse, causes them, every single time. Each task needs to move from an imperative shell command to a declarative module that checks the current state before acting, and the fix should be validated by actually running the playbook twice and confirming the second run reports zero changes, not just by reading the code and assuming it's fine.
Structured elaboration
Task 1: command: apt-get install -y nginx. A raw shell command has no idea whether nginx is already installed; it just runs apt-get install every single time. That might be a no-op at the package-manager level, but Ansible itself has no way to know that and will always report this task as "changed," which defeats the entire point of using a configuration-management tool.
Task 2: the copy task for the config file. This one is closer to idempotent already, since Ansible's copy module compares the destination file's content against what's being written and only reports a change when the content actually differs. It's still incomplete though: no explicit file permissions or owner are set, and a config change should trigger a service reload, not happen silently with no connection to the next task.
Task 3: service: state: restarted. This always restarts the service on every single run, whether or not anything actually changed. It's the least idempotent line in the whole playbook: running this playbook nightly, for example on a schedule, would bounce nginx nightly for no reason at all.
The fix. Use Ansible's notify/handler pattern: the config-file task notifies a handler, and the handler, which reloads or restarts the service, only runs when that specific task actually reported a change. A no-op run then touches the service zero times.
Worked example
A corrected version of the playbook:
- hosts: web
become: true
tasks:
- name: install nginx
apt:
name: nginx
state: present
- name: place nginx site config
copy:
content: "server { listen 80; }"
dest: /etc/nginx/sites-available/default
owner: root
group: root
mode: '0644'
notify: reload nginx
handlers:
- name: reload nginx
service:
name: nginx
state: reloaded
Why each change matters: the apt module is declarative, it checks the package's actual state first, so a rerun is cheap and honest about whether anything changed. notify plus a handler means the service only restarts, specifically via reloaded, which is less disruptive than a full restarted, exactly when the configuration actually changed, not on every run regardless of state.
How to test this in CI (continuous integration). Use Molecule, a testing framework built specifically for Ansible roles, to spin up an ephemeral container and converge (run) the playbook against it, then converge a SECOND time and assert the second run reports zero changed tasks, that's a direct, mechanical test of "is this actually idempotent," rather than trusting it by inspection. Add a verify step, using a tool like Testinfra, asserting the real end state: nginx is installed, the config file has the expected content, and the service is running. Wire this into the CI pipeline so a role that regresses on idempotency fails the build automatically, instead of being caught by a human rerunning it by hand much later.
Trade-offs and pitfalls
reloaded is gentler than restarted, but not every application supports a clean reload; some genuinely need a full restart to pick up certain kinds of configuration changes, so this substitution has to match how the real service actually behaves, not be applied blindly to every service task. Testing idempotency by running the playbook twice in CI adds real time to every pipeline run, a fair cost for something this cheap to verify and this easy to silently break without anyone noticing.
Unlock Full Question Bank
Get access to all 32 Code Review and Working with Existing Codebases interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.