Skip to content
Preptima

AI on Brownfield Codebases: A Masterclass for Interviews

What interviewers are testing when they ask how you use LLM tooling on a large existing system: comprehension of code nobody remembers writing, retrieval over a repository that will never fit a context window, characterisation tests as the highest-value application, the review burden inverting, hallucinated APIs, inherited bad conventions, silent behavioural regressions, and how to talk about your own experience without overclaiming.

Masterclass·70 min read

What it is

A brownfield codebase is one you did not write, cannot fully read, and are not allowed to stop. It has a decade of commits, three generations of framework conventions layered on each other, tests whose coverage number is meaningless because nobody knows which of them assert anything, and a set of behaviours that customers depend on which were never specified and in several cases were never intended. The people who made the original decisions have left, been promoted out of the code, or genuinely do not remember. What remains as documentation is a wiki page that was accurate two rewrites ago and a comment block that contradicts the function beneath it.

Applying a language model to that is a different activity from applying one to a new project, and the difference is not one of degree. On a greenfield project the model is generating code into a space you define: you hold the intent, you can read every line the model produced against that intent, and the codebase is small enough that you know whether the suggestion fits. Every one of those conditions fails on brownfield. The intent is distributed across code you have not read. The suggestion has to fit conventions you have not learnt. The verification you would use to check the output does not exist, because the reason this code is hard to change is precisely that it is not covered by tests. And the volume of context that would be needed to make a genuinely informed suggestion exceeds anything you can hand the model at once.

That last point is worth stating in its strongest form, because it is the structural fact underneath most of the rest. A repository is larger than any context window. This is not a claim about the current generation of models that will expire when windows get bigger; it is arithmetic. Windows are a constant and codebases grow, and the codebases where assistance would be most valuable are by definition the large ones. Every practical use of a model on existing code is therefore a retrieval problem wearing a generation problem's clothes, and the quality of the answer is bounded by the quality of the selection that preceded it. That is the same argument that applies to any corpus, and it holds here for the reasons set out in whether a large context window removes the need for retrieval.

The loop, and the branch that defines the subject

Work on existing code with a model attached is a loop rather than a transaction, and the loop has one branch in it that does not exist on greenfield.

What to look for is the branch out of the verification question. On a new project that branch is never taken, because the test was written moments ago by the same person holding the same intent, and the loop is a tight generate-and-check cycle whose feedback is fast and trustworthy. On existing code the branch is taken most of the time, and the detour it forces is the whole difference between the two situations: before the model's output can be checked at all, somebody has to establish what the current behaviour is, which is a harder and more interesting problem than the change that prompted it.

The task taxonomy

Not everything transfers equally badly. The useful mental model is that assistance transfers in proportion to how much of the task's information is present in the text you can show the model, and degrades in proportion to how much of it lives in history, in production behaviour, or in somebody's head.

TaskHow well assistance transfers to brownfieldWhy
Explaining what a subsystem doesWell, with caveatsThe code is the evidence and it is all textual; the failure mode is confident summary of a path that is dead
Writing characterisation testsVery wellThe oracle is the current behaviour, which the model can read, and volume is the bottleneck rather than judgement
Mechanical refactor across many filesVery wellThe transformation is specifiable, uniform, and checkable by the compiler or the existing suite
Framework or library upgradeModeratelyThe mechanical part transfers; the semantic changes buried in release notes do not, and those are what break you
Adding a feature in an unfamiliar areaPoorly at firstRequires knowing conventions, invariants and the six other callers, none of which are in the file you opened
Judgement refactor that changes a boundaryPoorlyThe reason to move the boundary is organisational and historical, and neither is in the source
Diagnosing a production incident from code alonePoorlyThe evidence is in telemetry and timing, not in the text
Deleting dead codeDeceptively poorlyReachability in a large system is a runtime property, and reflection, configuration and jobs defeat static reasoning

The rows worth memorising are the two at the extremes. Characterisation tests transfer best, and that is unintuitive because writing tests feels like the skilled part. Deleting dead code transfers worst while feeling like the safest possible task, which is exactly why it is the one that produces an incident.

What is different about the context problem here

Retrieval over prose and retrieval over code are not the same problem, and an interviewer who works on developer tooling will probe the difference. A document corpus is mostly self-contained at the passage level: a paragraph about the refund policy means roughly what it says. A code corpus is a graph whose nodes are individually meaningless. The function you retrieved does not tell you what its caller expects, what the interface it implements guarantees, which of its parameters is always null in practice, or that a subclass three modules away overrides the branch you are reading. Chunking source by token count severs exactly the edges that carry the meaning, which is a sharper version of the general problem described in choosing a chunking strategy.

The other difference is that code has a real structure you can traverse rather than approximate. There is a compiler, or at minimum a parser, that knows the definition of every symbol, the callers of every function, the implementations of every interface and the type of every expression. That is an exact index, available for free, of the relationships that embeddings can only guess at. Any serious answer to "how would you assemble context over a repository" reaches for that structure, and a candidate who proposes only embedding every file and doing nearest-neighbour lookup has described a system that will retrieve the four files most similar in vocabulary to the query and miss the one that matters because it shares no words with it.

Assembly strategyWhat it reliably retrievesWhat it misses
Semantic search over embedded chunksCode that talks about the same domain concepts in similar wordsCallers and callees that share no vocabulary, and anything named badly
Lexical and symbol searchExact identifiers, string literals, error messagesConcepts the query names differently from the code
Call graph and type traversal from a seedThe definitions and callers that determine behaviourRuntime dispatch, reflection, dependency injection, configuration-driven wiring
Directory and module structureNeighbouring code sharing conventionsCross-cutting concerns deliberately kept elsewhere
Version history for the target linesWhy a line exists, who changed it, what ticket it belonged toAnything decided in a meeting and never written down
Tests exercising the targetIntended behaviour, and the edge cases somebody once cared aboutBehaviour that is depended on but was never tested

The last two rows are the ones candidates forget and interviewers notice. History is the highest-signal context source in a brownfield repository, because the question you are usually trying to answer is not "what does this do" but "why is it like this", and that answer exists only in commits, in the linked ticket, and in the diff that introduced the strange conditional. Feeding the blame output for the lines being changed alongside the code itself is a small trick with a large effect, and mentioning it unprompted is a reliable signal that someone has done the work rather than read about it.