Skip to content

Senior Engineering Interview Handbook / Chapter 60

Pair Programming

A sustained pair-programming session shows how to make reasoning interruptible, absorb corrections without losing the thread, and finish a live coding change with shared confidence.

Keep two things moving

In a pair-programming round, the code must move and the other engineer must be able to steer it. Either condition on its own is inadequate. Silent speed conceals assumptions until they are expensive to undo; continuous explanation turns a coding session into a talk with a stalled cursor.

The useful middle is shared control. Before a consequential edit, make its purpose visible. After the edit, expose the result. When feedback changes the plan, say what changed and preserve whatever still holds. The interviewer should be able to answer three questions throughout the session: what are we trying to make true, why is this the next move, and what evidence will tell us it worked?

That is more demanding than narrating your thoughts. It requires you to choose which thoughts another engineer can act on.

A pair-programming collaboration loop cycles through align, propose, implement, check, adapt, and verify, with communication and code movement balanced.
Shared control is a short loop: align, propose, implement, check, adapt, and verify. The pair can re-enter it whenever uncertainty or feedback changes the work.

Make the next move interruptible

Suppose the prompt is brief:

This in-memory API-key cache sometimes returns expired entries. Make expiry behavior correct and add tests.

Do not begin with a speech about cache design. Establish the smallest working contract and open the collaboration channel:

“I’ll find the read path and the existing clock or time convention, then add a boundary test before changing behavior. I’ll treat an entry as expired at now >= expires_at unless the code or product contract says otherwise. Please interrupt if there is a cleanup or metrics requirement I haven’t found.”

This opening does useful work. It names the search target, identifies a boundary likely to cause a bug, proposes the first proof, and admits a relevant unknown. It does not ask permission for every keystroke.

Now search. A long silent browse through an unfamiliar repository leaves the other engineer unable to distinguish progress from confusion. A running commentary on filenames is no better. Surface only the shape of the search:

get delegates to the cache store, so I’m following that call rather than editing the handler. I’m also looking for a fake clock; using wall time in the test would make the boundary hard to control.”

If the search stalls, put a limit on it:

“I haven’t found expiry ownership yet. I’ll inspect the entry type and the nearest cache test next; if neither answers it, I’ll ask where cleanup is expected to live.”

These statements make the investigation reviewable without pretending you already know the codebase.

Let feedback change the code

The cache contains an injected clock and an entry with expires_at. Its read path currently resembles this:

get(key):
  entry = entries.get(key)
  if entry is missing:
    return missing
  return entry.value

The first test should settle the semantic boundary:

given an entry whose expires_at is 10:00:00
and a clock fixed at 10:00:00
get(key) returns missing

Before adding cleanup, say what you are deferring:

“I can make reads correct by returning missing for an expired entry. Removing it during the read would add mutation and possibly a concurrency obligation, so I won’t assume that behavior until I see the store contract.”

The interviewer then tells you that expired reads must increment an eviction metric and remove the entry. That is not a distraction. It changes the contract. A weak response is to drop a delete(key) into the branch and keep typing. A stronger response identifies the new risk:

“Then the read is also a cleanup path. I need to know whether a plain delete could remove a newer value written for the same key after this lookup. I’ll check for conditional removal and for the metric convention before editing.”

In this modeled codebase, you find remove_if_same(key, entry), which atomically removes the mapping only if it still points to the entry you read. Your first implementation now has a coherent shape:

get(key):
  entry = entries.get(key)
  if entry is missing:
    return missing

  now = clock.now()
  if now >= entry.expires_at:
    if entries.remove_if_same(key, entry):
      metrics.increment("cache_eviction", reason="expired")
    return missing

  return entry.value

Name the decisions while they are still cheap to challenge:

“I’m capturing now once so this read has one time observation. Conditional removal protects a replacement entry, and the metric increments only when this call actually removes the expired mapping.”

The interviewer points out another local convention: metrics record attempted expiry, even when conditional removal loses a race. Do not defend the first interpretation merely because it was reasonable. Restate the consequence:

“Under that convention, the metric belongs on the expired branch rather than inside successful removal. I’ll change that and add the expectation to the boundary test so the convention is visible.”

The branch becomes:

if now >= entry.expires_at:
  metrics.increment("cache_eviction", reason="expired")
  entries.remove_if_same(key, entry)
  return missing

You have accepted the correction without becoming passive. The technical reason is still explicit, and the test will prevent the detail from becoming a verbal promise.

Speak where the screen is silent

Useful narration supplies information the diff cannot show. Spend words on:

  • an assumption that changes behavior;
  • the invariant or boundary an edit protects;
  • the reason for choosing between plausible approaches;
  • uncertainty that changes where you search;
  • the consequence of feedback;
  • what a test just established;
  • a risk deliberately left outside the exercise.

The screen already shows syntax, scrolling, and simple renames. Describing them consumes attention without creating shared understanding.

Compare these two comments during implementation:

“Now I’m adding an if, then I’ll call remove, then increment the metric.”

“The expiry check precedes the return, and conditional removal prevents this stale read from deleting a replacement entry. I’ll run the exact-boundary test before adding the just-before-expiry case.”

The second comment gives the navigator something to review. It also commits the driver to a near-term verification step.

Silence is sometimes appropriate. A few seconds spent completing a coherent edit need no filler. If you are quiet because you are lost, expose the decision you are trying to make. If you are talking because you are afraid of silence, turn the explanation into a test, a search, or a small code move.

Use the navigator without surrendering judgment

The driver controls the keyboard; the navigator watches direction, edge cases, and consequences. In an interview those roles are loose, but the division is still useful. Driving means proposing the next move and keeping it small enough to review. It does not mean retaining exclusive control of the solution.

Invite help at decisions that can still change the work:

  • “Does expiry at the exact timestamp match the service contract?”
  • “I found both a background sweeper and cleanup on read. Which one owns the eviction metric?”
  • “This helper is shared by the refresh path; I’ll run those tests too.”

These are not requests for reassurance. Each question identifies a real fork, codebase convention, or blast radius.

Treat a correction as evidence. If it exposes a bug, turn it into a regression test. If it reveals a requirement, update the contract aloud. If it is a style preference, follow the local convention without manufacturing a design debate. If you think it would break the requirement, make the consequence inspectable:

“A plain delete(key) could remove a replacement written after our lookup. I’d like to keep remove_if_same; we can demonstrate the race with a focused store test if that behavior is in scope.”

The two failure modes are reflexive agreement and reflexive resistance. Shared control requires a third response: understand the suggestion, compare it with the contract, then choose the next move for a stated reason.

Finish with shared confidence

For the cache change, run the focused tests first. Prove that an entry is returned just before expiry, missing at the exact boundary, and missing after expiry. Prove the required metric behavior and, if the store fixture supports it, that conditional removal does not delete a replacement entry. Then run the nearby cache tests because the read path is shared.

Do not end when the last assertion turns green. Give the pair an exact account of the state you are leaving:

“Expired entries now read as missing at now >= expires_at. The cleanup uses conditional removal so a stale reader cannot delete a newer mapping, and the expiry metric follows the existing attempted-eviction convention. The boundary and adjacent cache tests pass. I did not change the background sweeper or define behavior for clock rollback; those would need separate contracts.”

That close is modest and useful. It distinguishes implemented behavior, evidence, and remaining uncertainty. It also gives the interviewer a clean place to ask a production follow-up.

Rehearse a conversation, not a monologue

Before attempting the complete practical-engineering mock, do one live rehearsal in a small existing codebase. Ask another engineer to give you a bounded change and to interrupt at least twice: once with a valid correction and once with a plausible alternative that you need to evaluate.

Choose work with enough resistance to require judgment but little enough code to finish: fix validation that mutates before rejecting input, add a missing cache boundary, change retry classification, or repair a state transition. During the rehearsal, protect four moments:

  1. State the contract and first proof before editing.
  2. Make searches and consequential edits legible.
  3. Turn feedback into a changed plan, a test, or a reasoned disagreement.
  4. Close with behavior, verification, and residual risk.

Afterward, ask your partner to describe the plan at the moment before each interruption. If they cannot, the problem is not a lack of talking. The next move was too large or its purpose remained private.

Pair programming does not require a performance of constant confidence. It requires work another engineer can enter while it is still being shaped. Keep the code moving, keep the reasoning interruptible, and let the result become safer because two people could influence it. The next chapter extends that discipline across a complete practical-engineering mock, where contract, implementation, tests, review, and refactoring must all remain connected.