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.
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.
flowchart TD
accDescr: The brownfield loop from selecting a target area and assembling context out of a repo too large to fit, through generating the change, to the branch that does not exist on greenfield, a check for whether any test covers the behaviour, which sends uncovered areas away to write a characterisation test first and reassemble context, while covered ones go to the suite, failures feeding back as context and passes reaching human review and merge.
A[Select a target area] --> B[Assemble context from a repo too large to fit]
B --> C[Generate the change]
C --> D{Is there a test that covers this behaviour}
D -->|no| E[Write a characterisation test first]
E --> B
D -->|yes| F[Verify against the suite]
F -->|fails| G[Feed the failure back as context]
G --> C
F -->|passes| H[Human review and merge]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.
| Task | How well assistance transfers to brownfield | Why |
|---|---|---|
| Explaining what a subsystem does | Well, with caveats | The code is the evidence and it is all textual; the failure mode is confident summary of a path that is dead |
| Writing characterisation tests | Very well | The oracle is the current behaviour, which the model can read, and volume is the bottleneck rather than judgement |
| Mechanical refactor across many files | Very well | The transformation is specifiable, uniform, and checkable by the compiler or the existing suite |
| Framework or library upgrade | Moderately | The mechanical part transfers; the semantic changes buried in release notes do not, and those are what break you |
| Adding a feature in an unfamiliar area | Poorly at first | Requires knowing conventions, invariants and the six other callers, none of which are in the file you opened |
| Judgement refactor that changes a boundary | Poorly | The reason to move the boundary is organisational and historical, and neither is in the source |
| Diagnosing a production incident from code alone | Poorly | The evidence is in telemetry and timing, not in the text |
| Deleting dead code | Deceptively poorly | Reachability 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 strategy | What it reliably retrieves | What it misses |
|---|---|---|
| Semantic search over embedded chunks | Code that talks about the same domain concepts in similar words | Callers and callees that share no vocabulary, and anything named badly |
| Lexical and symbol search | Exact identifiers, string literals, error messages | Concepts the query names differently from the code |
| Call graph and type traversal from a seed | The definitions and callers that determine behaviour | Runtime dispatch, reflection, dependency injection, configuration-driven wiring |
| Directory and module structure | Neighbouring code sharing conventions | Cross-cutting concerns deliberately kept elsewhere |
| Version history for the target lines | Why a line exists, who changed it, what ticket it belonged to | Anything decided in a meeting and never written down |
| Tests exercising the target | Intended behaviour, and the edge cases somebody once cared about | Behaviour 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.
Why we need it
The reason this has become its own interview topic, rather than a sub-question of AI-assisted development generally, is that the published material and the professional reality point in opposite directions, and interviewers have noticed the gap.
Almost every demonstration of coding assistance is greenfield. A new app, an empty directory, a specification stated in a paragraph, output that runs in a minute. Those demonstrations are not dishonest, and the capability they show is real, but they select for the conditions in which the tooling is strongest: no existing conventions to infer, no untested behaviour to preserve, no unfamiliar invariants, and a codebase small enough to fit entirely in the prompt. Professional engineering mostly does not happen in those conditions. The median engineer joining a company is handed a system that predates them, and the value of assistance to their employer depends almost entirely on how it behaves in the conditions the demonstrations exclude.
So interviewers are testing calibration. They want to know whether a candidate's enthusiasm was formed on side projects, in which case it will not survive contact with the estate, or on real work, in which case it will come with a set of qualifications attached. This is why the strongest thing you can do in one of these conversations is volunteer the failure modes. Someone who says the tooling made them faster and stops has told the interviewer nothing they can use. Someone who says it made them faster at three specific classes of work, slower at one, and dangerous at one more has demonstrated that they have a model of when to reach for it.
There is a second reason, which is that the economics of the review step invert. Generating a change is now cheap and reviewing it is not, and on unfamiliar code the review is harder than the writing would have been. When you write a change yourself you build understanding as you go: by the time you have a diff, you have already read the surrounding code, discovered the second caller, noticed the odd null check and formed a view about it. A generated diff arrives with none of that. You are handed the conclusion of a reasoning process you did not perform, over code you do not know, and you must reconstruct enough understanding to judge it. For a small diff this is fine. For a four-hundred-line diff touching eleven files in a subsystem you have never opened, the honest reviewer's cost is close to the cost of doing the work, and the dishonest reviewer's cost is zero, which is the problem. Volume that would previously have been rate-limited by the effort of producing it is no longer rate-limited by anything except the willingness to approve it.
Third, the accountability question has no technical answer and organisations are being forced to give it one anyway. A change that causes an incident was authored by somebody. If the answer to "who wrote this" is a tool, the answer to "who is responsible" has to be a person anyway, and every functioning engineering organisation has landed in the same place: the person who submitted it. Interviewers ask about this because it is a values question disguised as a process question, and because a candidate who treats generated code as something that arrives pre-blessed is a liability in a regulated environment and a nuisance in an unregulated one.
Fourth, there are hard constraints that simply do not arise on a personal project. Proprietary source code leaving the network is a data-handling decision with contractual and sometimes regulatory weight, particularly where the code embeds customer data in fixtures, credentials in configuration, or algorithms the business considers its differentiator. Where the model runs, what is retained, whether prompts are used for training, and whether a subprocessor in another jurisdiction is involved are all questions somebody has to answer before the first line is generated. Those are the same questions any third-party processing raises, discussed generally in adding an AI provider in another country, and they land harder on source code than on most data because the asset and the tooling are the same object.
Finally, the systems where this matters most are the ones where nobody remembers the decisions. That is not a complaint about documentation; it is the normal end state of any long-lived system, and the reason inheriting a system nobody understands is a standing interview scenario. What is new is that there is now a tool that will produce a plausible explanation of that system on demand, in fluent prose, with no signal attached about whether it is right. That is genuinely useful and genuinely hazardous, and holding both of those at once is most of what is being scored.
Where the assistance is strongest
Comprehension of a subsystem nobody understands
The first thing anyone does with a model on an unfamiliar codebase is ask it what the code does, and this is a good instinct. Reading a thousand lines of somebody else's control flow to extract a three-sentence summary is exactly the kind of mechanical-but-tedious work where a model earns its place, and the output is a hypothesis you can test rather than a change you have to trust. The risk profile is favourable: a wrong summary costs you the time to discover it is wrong, whereas a wrong change costs you an incident.
The technique that separates a useful comprehension pass from a decorative one is to ask for things that are falsifiable. "Explain this module" produces fluent prose that is hard to check. "List every external system this module writes to, and for each one quote the line that does the write" produces a set of claims you can verify in a minute, and the verification is the point. So does asking for the preconditions a function assumes, the invariants it maintains, the cases in which it returns early, and the inputs for which it would throw. Each of those is a claim about specific lines, and each can be checked against those lines. Asking for a narrative gives you something that reads well and cannot be graded; asking for claims gives you something that can be wrong in a visible way.
The characteristic failure here is confident summary of a path that is dead. A model reading a function with three branches will describe all three as though the system takes them, when in production the configuration flag has been false for four years, the second branch is unreachable, and the whole complexity of the function is vestigial. Nothing in the source says so. The signal lives in production telemetry, in the configuration repository, and in the fact that nobody has touched those lines since a migration. This is the general reason a comprehension answer should always be paired with runtime evidence: add a counter, look at the logs, check the feature flag's current value in each environment. Candidates who say this unprompted have debugged something real.
The related failure is that a model asked to explain code will explain it. It will not say "this is incoherent" or "these two branches contradict each other" or "this whole class appears to be a half-completed refactor" unless you ask it to look for that, because the request was to explain and explanation is what it produces. Asking explicitly for the parts that look inconsistent, the code that appears unreachable, and the assumptions that seem to conflict is a different prompt that surfaces different material, and on a legacy subsystem it is usually the more valuable one.
Characterisation tests for untested legacy code
This is the highest-value application of a model to existing code, and stating that clearly and defending it is one of the strongest things a candidate can do in this conversation.
A characterisation test does not assert that the code is correct. It asserts that the code does what it currently does. You write it not because you approve of the behaviour but because you are about to change something nearby and you need a tripwire that fires if you alter behaviour you did not intend to alter. It is the standard technique for getting a grip on code you are afraid to touch, and it is unglamorous, high-volume, low-judgement work: pick an input, run the code, observe the output, pin it. The reason so much legacy code has none is not that engineers do not know the technique. It is that writing four hundred of them is a week nobody has, and the payoff is invisible until the day it saves you.
Volume-limited, low-judgement, mechanically checkable work is precisely the shape that assistance fits. The model can read the function, propose inputs that exercise each branch, generate the test scaffolding, and — critically — you do not have to trust its idea of what the output should be, because you are not asserting correctness. You run the code and record what comes back. The oracle is the system itself.
# A characterisation test pins behaviour rather than asserting correctness.
# Nobody thinks a negative quantity should produce a positive discount.
# The test exists so that if a refactor changes it, we find out deliberately.
def test_discount_for_negative_quantity_is_unchanged():
result = pricing.discount(unit_price=10.00, quantity=-3, tier="gold")
assert result == 4.50 # observed from the current implementation, not derived
def test_discount_for_unknown_tier_falls_through_to_gold():
# Undocumented, almost certainly accidental, and two customers depend on it.
assert pricing.discount(10.00, 1, "platinum") == pricing.discount(10.00, 1, "gold")
The comment on the second test is the part that matters, and it is the part a model will not write for you. A pinned value with no note beside it is a trap for the next engineer, who will read the assertion, conclude it encodes intended behaviour, and preserve it forever. The convention worth adopting, and worth describing in an interview, is that every characterisation test carries a one-line note saying whether the pinned behaviour is believed intentional, believed accidental, or unknown. That single distinction turns a wall of assertions into a map of where the system is load-bearing and where it is merely habitual.
There is a second-order benefit that senior candidates reach for. Generating characterisation tests over a subsystem forces enumeration of its inputs, and enumeration surfaces the cases nobody had thought about. The exercise of asking a model for twenty inputs that reach twenty different paths through a function routinely produces three that make the author say "what happens if that is null?" — and the answer is frequently interesting. The tests are the deliverable; the enumeration is the insight.
Two cautions belong with this. First, a generated test that passes on first run has told you nothing until you have confirmed it can fail. A test asserting something trivially true, or asserting against a mock so thorough that no production code executes, is worse than no test because it consumes review attention and provides false assurance. Mutate the code, confirm the test goes red, revert. Second, coverage as a number will move dramatically and mean very little, for all the reasons in what ninety percent coverage does not tell you. Coverage measures execution, not assertion, and a suite of generated tests is exactly the artefact most likely to execute a great deal while asserting very little.
Mechanical refactors at scale
The other place assistance is straightforwardly strong is transformations that are uniform, specifiable and checkable. Renaming a concept across two hundred files. Replacing a deprecated call with its successor everywhere. Converting a callback style to a promise style. Adding a parameter to an internal interface and threading it through every implementation. Normalising a logging call so it carries a correlation identifier. These share three properties: the transformation is the same everywhere, a reviewer can state the rule in one sentence, and a compiler or existing test suite will catch a large fraction of the errors.
The judgement in an interview answer here is about the boundary. A candidate who says "use the model for mechanical refactors" is repeating a slogan. A candidate who says that where a deterministic tool exists you should prefer it — the language's own rename, a codemod, an abstract-syntax-tree transformation — because it is exact and reviewable as one rule rather than two hundred instances, and that the model's place is generating the codemod or handling the residue the codemod could not express, has said something an experienced person says. A ninety-eight percent mechanical refactor is the dangerous case: the codemod handles the uniform part, and the two percent that required judgement is where the defect lives, so the review must be concentrated there rather than spread evenly.
The complementary point is that a large uniform diff and a large non-uniform diff need completely different review strategies, and conflating them is how bad changes get approved. If the rule is genuinely uniform, review the rule and spot-check the instances, and require the diff be mechanically verifiable — regenerate it and confirm you get the same output. If it is not uniform, the diff has to be split, because there is no honest way to review four hundred distinct judgements in one sitting.
Assisted upgrades and framework migrations
Version upgrades sit in between, and they are a good interview topic because the naive answer and the correct answer are visibly different.
The naive answer is that an upgrade is a large mechanical refactor and therefore transfers well. The mechanical part does: renamed packages, changed method signatures, replaced annotations, configuration that moved. A model with the migration guide in context is genuinely good at that, and it removes a great deal of tedium.
The correct answer notes that the mechanical part is not what breaks you. What breaks you is the semantic change that compiles cleanly. A default that changed. Serialisation that now emits a field it used to omit. A connection pool whose eviction policy is different. A date parser that became stricter. A transaction boundary that moved. Ordering that used to be incidental and is no longer. These produce code that builds, passes a thin test suite, and behaves differently in production under load or at a month boundary. They are documented in release notes as a sentence in a long list, and they are exactly the class of thing a model summarising a migration guide will compress away.
So the shape of a good answer is: use assistance for the mechanical sweep, use the release notes as a checklist of behavioural changes to be verified individually against your own usage, and get the behavioural safety from characterisation tests written before the upgrade begins. The tests are the deliverable that makes the upgrade tractable, which is the same conclusion the previous section reached by a different route, and saying so explicitly shows you are reasoning from a principle rather than listing techniques. Where the system cannot be taken down for the migration at all, the sequencing question becomes an architectural one of the kind covered in replacing a system that cannot be switched off; the point to hold here is that the AI tooling changes the cost of the mechanical work and changes nothing about the sequencing.
Where it degrades, and how each failure is caught
The failure modes are distinct, they have different causes, and they are caught by different mechanisms. Treating them as one undifferentiated "sometimes it is wrong" is the weakest possible answer, and enumerating them precisely is one of the clearest seniority signals available in this conversation.
| Failure mode | What it looks like | Caught by |
|---|---|---|
| Hallucinated API | A method, parameter, flag or configuration key that does not exist | The compiler, in a typed language; nothing, in a dynamic one, until the branch runs |
| Plausible but wrong internal call | A real function from your codebase used with the wrong contract | Types if you are lucky; otherwise a test that exercises the path, or production |
| Inferred bad convention | Correct, consistent with the surrounding code, and reproduces a pattern you were trying to eliminate | Human review by someone who knows the intended direction; no automated check exists |
| Silent behavioural regression | Behaviour changed in a case nothing asserts and nobody documented | A characterisation test written before the change, or a customer |
| Overreach in scope | The requested change plus four unrequested improvements | Diff review, and a discipline of rejecting unrequested changes outright |
| Confidently wrong explanation | A fluent summary of code that does not do that | Falsifiable prompting, plus runtime evidence |
| Deleted code that was reachable | Removal justified by static reasoning, defeated by reflection or configuration | Production, usually at month end |
Hallucinated APIs, and why unfamiliarity is the multiplier
A model inventing a method that does not exist is the best-known failure and, in a strongly typed language on a well-known framework, close to harmless. The build fails, you read the error, you move on. It is annoying rather than dangerous.
The danger is a function of what you can independently verify. In a framework you know well, an invented method reads wrong immediately: you have a mental index of the API and the suggestion is not in it. In an internal codebase you have never seen, you have no index. A call to AccountService.reconcileBalance(accountId, asOf) looks exactly as plausible whether it exists, whether it exists with different semantics, or whether it was deleted in a refactor eighteen months ago and the model saw its ghost in a comment. You cannot tell by reading, which means the only defences are mechanical and social: does it compile, does an existing test exercise it, and does the reviewer know the subsystem.
The worse version is not an invented symbol but a real one misused. The model finds AccountService.reconcileBalance because it genuinely exists, calls it, and the code compiles. What it does not know is that this method must never be called inside the batch window, that it assumes the caller already holds the ledger lock, or that it silently returns a stale figure when the cache is cold. Those constraints live in a comment on a different class, in a runbook, in the memory of someone who left. No static check catches this. The interviewer is listening for whether you know that the compiler's silence is not evidence, and for whether your process puts a human who knows the subsystem in the path of any change that touches it. Requiring an owning-team review for changes inside a sensitive module is an unglamorous answer and the right one.
The general habit that covers this class is refusing to let unverified symbols through. Every external API in a generated change gets checked against the actual documentation for the actual version you depend on, and every internal one gets checked against the definition. It is a slow habit and it is the one that separates people who have shipped generated code from people who have demonstrated it.
Inherited conventions, or the model reproducing your worst pattern faithfully
This one is subtle and it is the failure that interviewers with real experience like most, because it exposes whether a candidate understands what a model conditioned on a codebase is doing.
Given surrounding code as context, a model will infer the conventions of that code and follow them. This is what you want most of the time; it is why context helps. But the conventions of a legacy codebase include its mistakes. If every service in the module catches a broad exception and logs it at debug level, the generated service will too. If data access happens by string concatenation because the module predates the team's parameterised-query rule, the generated data access will concatenate. If the codebase carries a home-grown result type that everybody agrees was a mistake and is slowly being replaced, the model will use it, because it appears in ninety percent of the neighbouring code and its replacement appears in five.
The output is therefore correct, consistent, review-passing, and pointing in exactly the wrong direction. There is no automated check for this, because by every mechanical measure it is fine: it compiles, it matches the file's style, the linter is happy, the tests pass. It is wrong only relative to an intention that exists in the team's head and, if you are fortunate, in a decision record. This is one of the more practical arguments for keeping architectural decisions written down where the tooling can read them, in the spirit of what belongs in an ADR and when to write one — a convention that exists only as a shared understanding among four people cannot be given to a model as context, and cannot be enforced when three of those four are not on the review.
The mitigations are all about making the intended direction explicit and machine-readable. State the target pattern in the instructions rather than hoping it is inferred. Point the context at the module that already does it correctly rather than at the module you are editing. Write the rule as a lint check where it can be expressed as one, because a lint rule is a convention with teeth and a wiki page is a convention with none. And accept the residual: on a codebase mid-migration between two patterns, the model will pull towards the old one because there is more of it, and the only real defence is a reviewer who knows which way the team is going.
There is a compounding effect worth naming, because staff-level candidates raise it and it lands well. If generated code follows the majority pattern, and generated code becomes a growing share of what is committed, then the majority pattern gets more majority. A codebase that is sixty percent old pattern and forty percent new is drifting backwards, and the drift is invisible in any individual review because each change is locally reasonable. The countermeasure is to finish migrations rather than leaving them at forty percent, which was always the right advice and now has a mechanism behind it.
Silent behavioural regressions
The most expensive failure in this whole subject is a change to behaviour that nothing asserts and nobody intended to depend on.
The mechanism is straightforward. A system that has been in production for years has accumulated behaviours that were never designed. A field that happens to come back sorted because of the index the query uses. An error case that happens to return an empty list rather than raising. A rounding that happens to go towards zero because of the type it passes through. A timestamp that happens to be in the server's local zone. None of these were decided. All of them are now depended on, by an integration you do not own, a report someone built, a downstream job, or a customer's script.
A refactor that is correct by every reasonable standard can change any of them. The generated version of a function may be cleaner, faster and more readable while returning results in a different order, and the order was load-bearing for a consumer nobody told you about. The change passes review because the reviewer is checking whether the new code is good, not whether it is identical, and identity is what was needed.
This is why characterisation tests come before the change rather than after it. Written first, they are a snapshot of reality and they fire when reality moves. Written after, they pin whatever the new code does, which is a record of the regression rather than a defence against it. Saying this crisply in an interview is worth a lot, because it demonstrates that you understand what the tests are for rather than merely that you have heard of them.
The complementary defence is production comparison for anything high-stakes. Run both implementations against real traffic, compare the outputs, and treat every difference as something to be explained rather than tolerated. Differences you can explain are fine. Differences you cannot are the ones you were looking for. This costs real effort and is not justified for every change, and part of the judgement being assessed is knowing which changes warrant it: the ones where the blast radius is wide, the consumers are external, or the behaviour touches money.
Judgement refactors, and why they do not transfer
A mechanical refactor has a rule. A judgement refactor has a reason, and the reason is usually not in the code.
Splitting a service because two teams keep colliding in it. Merging two modules because the boundary between them was drawn around an organisational structure that no longer exists. Moving a responsibility because the thing that made it belong there was a vendor you have since replaced. Choosing to leave a piece of ugliness alone because it is stable, unread, and about to be deleted. None of those are derivable from the source. They come from knowing what the team is trying to do, what changed in the business, which parts of the system are about to be replaced, and where the political cost of a change exceeds its technical benefit.
Ask a model to improve the structure of a subsystem and you will get a defensible answer, usually along the lines of the shape recommended by the most-published patterns for that language. Whether it is the right answer for this system depends on facts the model was not given and often could not be given. The failure is not that the suggestion is bad in the abstract; it is that it is generic, and the reason your system is not generic is what you would need to explain to get a useful answer. Where the argument itself is the deliverable — should this be a modular monolith or a set of services, as in modular monolith versus microservices — the tooling can enumerate options and stress-test your reasoning, which is genuinely useful, but it cannot hold the context that decides between them.
What a model is good at in this territory is being an argumentative reader. Describe the change you intend and ask what breaks. Ask what the previous team might have been solving that you have not noticed. Ask for the strongest case against your plan. That is a use of the tool that respects where the judgement lives, and describing it that way in an interview reads as maturity rather than as scepticism.
The review burden
The claim to make, and defend, is that a large generated diff is harder to review than the equivalent change would have been to write. It sounds contrarian and it is straightforwardly true once you separate the two activities.
Writing a change on unfamiliar code is a process of accumulating understanding. You open the file, you fail to understand it, you read the caller, you find the second implementation, you notice the null check and go looking for why, you discover the ticket from three years ago, you make the change, and the change is now the least interesting thing you produced. The understanding was the product. The diff is the residue.
Reviewing a generated change gives you the residue without the product. You must build enough understanding to judge a set of decisions you did not make, over code you do not know, with no record of the reasoning. And you must do it against an artefact optimised to look finished: consistent style, plausible naming, comments in the right places, no obvious loose ends. A hand-written change by a junior engineer signals its own uncertainty — the awkward variable name, the commented-out line, the question in the pull request description. Generated code signals nothing, because fluency is uniform regardless of correctness. The reviewer loses the cues they were unconsciously relying on.
Three consequences follow, and stating them is what separates a real answer from a general worry about review quality.
Size discipline stops being a stylistic preference and becomes a hard control. The argument for small diffs used to be about reviewer attention. Now it is about the only remaining verification step in the pipeline. If the diff exceeds what a human will genuinely read, then nothing has verified the change, and the process has a hole in it that no amount of tooling fills. A team that accepts eight-hundred-line generated changes has not sped up review; it has stopped reviewing.
The submitter's obligation changes shape. It is no longer sufficient to say what the change does. The submitter has to have understood it themselves, and the honest test is whether they can explain each non-obvious decision without re-reading. Some teams formalise this by requiring the pull request description to state which parts were generated and what the author verified. Whether the labelling is worth the friction is a real debate — it can become a ritual, and it can become a way to distribute blame — but the underlying obligation is not debatable, and an interviewer will listen for whether you locate responsibility in the submitter without hedging.
Review effort should be allocated by risk rather than spread evenly. A four-hundred-line change is not four hundred units of equal risk. The generated boilerplate, the test scaffolding and the mechanical rename need a glance. The three lines that touch a boundary, a transaction, a permission check, a money calculation or a public contract need the whole budget. A reviewer who reads a large diff at uniform attention will run out of attention before reaching the part that matters, which is why the submitter's job includes pointing at it.
The mechanism that makes all of this durable, though, is that the diff should not be the first line of defence. Anything mechanically checkable should have been checked mechanically before a human looked: the build, the type checker, the linter, the existing suite, the security scan, dependency and licence checks, and a mutation check on new tests if you have one. The human review should be spending itself on the things no tool can assess — whether this is the right change, whether it fits where the system is going, whether it silently altered something. Every mechanical check you add gives the human back attention for the part only they can do, which is the same logic that governs which tests run per commit and which run nightly.
Constraints that only exist at work
The data boundary
Source code is a business asset and frequently an encumbered one. Before any of this happens, somebody has to have answered where the code goes when it leaves the machine, who processes it, in which jurisdiction, what is retained and for how long, whether it is used to train anything, and whether the arrangement is covered by an agreement the legal team has read. These are ordinary third-party processing questions and they have ordinary answers, but the answers differ by organisation and a candidate who has never encountered the question is visibly a candidate who has only done this on personal projects.
Two specifics are worth being able to name because they are the ones that trip teams up. First, source code carries more than code. Test fixtures contain real customer records more often than anyone admits, configuration files contain credentials and internal hostnames, and comments contain the name of the customer who demanded the workaround. A policy that says "the code is fine to send" has usually not considered what is in the repository besides code, and the handling obligations are the same ones described in handling PII in prompts, logs and traces. Second, the boundary is rarely uniform across the estate. Most organisations that think about this land on a tiering: some repositories can be sent to a hosted service, some can only go to a deployment inside their own boundary, and some cannot leave at all. Being able to describe that shape, and to say that the constraint is per-repository rather than per-company, is a small detail that reads as first-hand.
Licensing and provenance of what comes back
The mirror-image concern is what the generated code is. A model trained on public repositories may produce output resembling code under a licence whose obligations you have not met, and the risk is not evenly distributed: it concentrates in code that is distinctive enough to be recognisably from one source, which usually means a well-known algorithm implementation rather than ordinary business logic. The practical controls are unremarkable — the same licence scanning you already run on dependencies, applied to your own source; a policy on generated code in components you distribute rather than merely operate; and attention to the specific case of a long, distinctive block appearing whole.
Provenance is the other half. In a regulated environment, the question of who authored a change and on what basis is not philosophical, because an auditor will ask it and "the tool wrote it" is not an answer that survives. The workable position is that authorship attaches to the person who submitted the change, that the review record is the evidence of human judgement, and that the fact of generation is a property of how the change was produced rather than a transfer of responsibility. Some organisations record generation in commit metadata; some record it in the pull request; some deliberately record nothing, on the argument that the author is accountable either way and a flag invites the reviewer to relax. All three are defensible and an interviewer is listening for whether you can argue the trade-off rather than for which one you picked.
| Constraint | The question somebody has to answer | Where teams get caught |
|---|---|---|
| Egress of source | Which repositories may leave the network, to whom, under what agreement | Treating the policy as company-wide when it is per-repository |
| Contents beyond code | What personal data, credentials and customer names sit in fixtures and comments | Approving "the code" without auditing the repository |
| Retention and training | What the provider keeps, for how long, and whether prompts train anything | Assuming the default tier and the negotiated tier are the same |
| Licence of output | Whether distinctive generated blocks carry obligations | Scanning dependencies but never scanning first-party source |
| Provenance | Who is accountable for a generated change | Answering with a tool rather than a person |
| Regulated evidence | What an auditor is shown to demonstrate human judgement | Having no review record for changes that were approved quickly |
Evaluating whether the tooling helped
At some point somebody senior asks whether any of this is working, and the answer has to be better than an anecdote and better than a vendor's number. The framing that survives scrutiny is to treat the question the way you would treat any change to a system whose effect you cannot directly observe: define what would count as evidence before you look, prefer things you can measure without the measurement changing behaviour, and be honest about what you cannot separate.
Three cautions belong in any answer here. Volume measures are not outcome measures, and any figure counting suggestions, lines or diffs will move in the direction the tool's presence pushes it regardless of whether anything improved. Self-reported perception is worth collecting because engineer experience genuinely matters, and is worth nothing as evidence of throughput because it is confounded by novelty and by wanting the new thing to be good. And the population using the tooling selects itself, so the comparison between adopters and non-adopters measures who adopted rather than what adoption did.
What is left is narrower and more useful. On brownfield specifically, the honest questions are whether work that used to be refused now happens, and whether the defect profile changed. The first is the most convincing evidence available: if a subsystem that nobody would touch because it had no tests now has tests and is being changed, something real happened, and it is directly attributable. The second is the risk side of the same ledger: whether the share of incidents traceable to changes in unfamiliar code went up, whether the time between a change landing and its defect surfacing got longer, and whether review is finding the same proportion of problems it used to. A team that can answer the first question and refuses to guess at things it has not instrumented is a team that has thought about this, and the discipline is the same one described in proving a fine-tune helped — decide what would falsify the claim before you start collecting numbers that support it.
Keep this brief in an interview unless asked to go deeper, because it is a large subject in its own right and the brownfield-specific part of it is small. What you want to demonstrate is that you would not accept a volume metric as evidence, and that you know which of the plausible measurements are confounded.
What interviewers ask
The conversation follows a small number of shapes, and each is graded on different observable behaviour. Recognising which one you are in tells you what to display.
| Question archetype | How it is usually phrased | The signal being graded |
|---|---|---|
| Calibration | How do you use AI tooling day to day | Whether the answer differentiates by task type instead of being uniformly positive or dismissive |
| Comprehension scenario | You have two weeks to understand a subsystem nobody owns | Whether the output is treated as a hypothesis and paired with runtime evidence |
| Context and retrieval | The repository does not fit. How do you decide what to send | Whether structure and history are used, not just embeddings |
| Verification | You cannot test the change. Now what | Whether characterisation tests come before the change, and whether the oracle is named |
| Failure enumeration | What goes wrong, and how do you catch it | Whether the failure modes are distinguished and matched to different detection mechanisms |
| Review process | How does your team review generated changes | Whether responsibility lands on a person, and whether size is treated as a control |
| Constraints | Can you send this code to a model | Whether the answer is per-repository, mentions what else is in the repo, and reaches provenance |
| Honesty probe | What did it not help with | Whether the candidate can name a real limitation from experience rather than a generic caveat |
The calibration question
Almost every one of these conversations opens with a version of "how do you use these tools", and the question is doing more work than it appears to. The interviewer is establishing a baseline for everything that follows, and there are two ways to fail it immediately.
Uniform enthusiasm fails because it tells the interviewer your experience is shallow. Anyone who has used this on a real system for six months has a list of things it is bad at, and producing that list is the fastest way to establish that your positive claims are worth listening to. Uniform dismissal fails for the mirror reason: it signals that you have decided rather than measured, and on a team that has adopted the tooling you will be the person arguing rather than the person improving it.
What passes is differentiation by task. Naming three things it is reliably good at, one thing it is unreliable at, and one thing you will not use it for, each with the mechanism attached, answers the question they were really asking, which is whether you have a model of when to reach for it.
The comprehension scenario
You are dropped into an unfamiliar subsystem with a deadline and asked how you would proceed. Four behaviours are graded.
Whether you treat the model's output as a hypothesis. Candidates who say they would ask for an explanation and then work from it have skipped the step the question exists to find. The strong version names the verification: falsifiable prompts, claims checked against the lines they cite, and a plan for confirming the explanation against how the system behaves at runtime rather than only against how it reads.
Whether you use history. A candidate who never mentions the commit log has not worked on old code, because on old code the question is almost always why rather than what, and why lives in the diff that introduced the oddity.
Whether you know what the model cannot see. Production configuration, the actual traffic mix, which branches are live, what the on-call rotation knows, and the fact that the third-party integration was turned off last year. Naming the sources you would go to for those is the difference between a plan and a prompt.
Whether the output of the exercise is durable. Understanding that lives only in a chat log has to be reacquired by the next person. Writing it down — as a decision record, a diagram, a set of tests — is what makes the effort compound, and the tests are the best form of it because they cannot go stale silently.
The verification scenario
This is the highest-signal question in the subject and the one that most cleanly separates people. You are told there is no test coverage for the area you must change, and asked how you get confidence.
The graded content is whether characterisation tests appear, whether they appear before the change rather than after, and whether you can articulate why the distinction matters. Candidates who say they would write tests are giving the right shape of answer with the important half missing; candidates who say they would write tests that pin current behaviour, including behaviour they believe is wrong, and that they would do it before touching anything, have said the thing the question was looking for.
Beyond that, the follow-on signals are whether you name the oracle — what tells you the expected value, and the answer is the current system, not your judgement — whether you know a passing generated test proves nothing until you have seen it fail, whether you distinguish behaviour that is intentional from behaviour that is accidental in what you pin, and whether you have anything to say about high-stakes cases where a test suite is not enough and output comparison against real traffic is warranted. Interviewers also listen for whether you would accept a coverage number as evidence, which is a small trap and a fair one.
The context and retrieval question
More common when the role touches developer tooling or when the interviewer is technical about the subject. The scored signals are whether you reach for the structure that already exists rather than treating code as undifferentiated text, whether you can say what each retrieval strategy misses, and whether you understand that the selection bounds the answer. A candidate who explains that a wrong answer is more often a retrieval failure than a generation failure — the diagnostic distinction drawn in did retrieval or generation fail — is applying a principle rather than reciting a technique, and that transfers.
The secondary signal is whether you know why naive similarity search over code underperforms. Code is a graph, its identifiers are frequently uninformative, and the function that matters may share no vocabulary with the question. That is the specific reason lexical and structural retrieval remain necessary alongside semantic retrieval, which generalises the argument in why dense retrieval alone is not enough.
The failure-enumeration question
Asked directly — "what goes wrong?" — and graded on differentiation. A candidate who says "it hallucinates" has named one failure and the least interesting one. A candidate who separates invented symbols from misused real symbols, inferred bad conventions from silent behavioural regressions, and attaches a different detection mechanism to each has demonstrated a working model.
The specific thing interviewers listen for is the inferred-convention failure, because it is the one you only learn by hitting it. It also happens to be the one with no automated defence, which makes it a good vehicle for the follow-up about where human review is irreplaceable.
The review-process question
Scored on where responsibility lands and on whether size is treated as a control rather than a preference. The answer must put accountability on the submitter without qualification. It should treat unreviewable diff size as a process defect rather than as an unfortunate reality. It should allocate review attention by risk. And it should push everything mechanically checkable ahead of the human, so the human's attention is spent on the questions only a human can answer.
A useful thing to volunteer is what you would refuse. A large generated change to a payment path, a permissions check or a data-migration script, submitted by someone who cannot explain it, should be sent back regardless of whether it passes. Saying so signals that you understand review as a control rather than as a courtesy.
The constraints question
Often asked casually and easy to underplay. The signals are whether you know the answer varies by repository, whether you think about what else is in the repository besides code, whether you get to retention and training, whether you reach provenance and accountability, and whether you can hold the position without becoming an obstacle. A candidate who says the policy is somebody else's problem has answered honestly and unimpressively; one who says which questions they would want answered before adopting the tooling on a given codebase has shown they can operate in an organisation.
The honesty probe
Somewhere near the end, some version of "where did it not help?" or "tell me about a time it produced something wrong". This is not a trick and it is not a humility test. It is a check on whether your other answers came from experience.
The failure is a generic caveat: "you always have to check the output". True, universally known, and evidence of nothing. What lands is a specific incident with a mechanism: what it produced, why it looked right, what caught it, what you changed afterwards. Even a small one is worth more than a large abstraction, because specificity is the thing that cannot be fabricated convincingly.
How the same question is scored at different levels
At mid level the bar is a working practice: you use the tooling, you verify the output, you know the common failure modes, and you do not treat a green build as proof. At senior level that is assumed, and grading moves to whether you can reason about the mechanism — why unfamiliarity multiplies hallucination risk, why the review economics invert, why characterisation tests are the leverage point — and whether you volunteer limits without being pushed. At staff level it moves again, to whether you can set policy: which repositories, which task classes, which controls, what evidence would tell you it was working, and what you would stop doing. Candidates who bring only personal-practice answers to a staff loop are routinely downlevelled, and the feedback reads as strong engineer, no view on the system.
Tells that the experience is shallow
Interviewers keep a short informal list. Talking only about greenfield generation when asked about existing code. Treating a passing build as verification. Not knowing what a characterisation test is, or knowing the term but placing the tests after the change. Assuming a large enough context window solves the repository problem. Having no view on where the code goes. Attributing authorship to a tool. Never having seen a generated change reproduce a pattern the team was trying to remove. Quoting a productivity figure from a vendor. None is fatal alone, and all are noted.
Questions
These are phrased the way interviewers phrase them. What follows each is the substance a strong answer contains, not a script.
Working on code you do not understand
You have joined a team and been given two weeks to understand a subsystem nobody currently owns. How do you use AI tooling, and what do you not use it for?
Start with the shape of the problem: you need a map, a set of invariants, and a sense of what is live, and only the first of those is cheaply available from the source. Use the model for the map. Ask for the entry points, the external systems written to, the data owned versus borrowed, and the boundaries between the pieces, and demand that every claim cite the lines supporting it so you can check them. That converts an unverifiable narrative into a set of falsifiable assertions, and checking twenty of them takes an hour and tells you how much to trust the twenty-first.
Then go to the sources the model cannot read. The commit history for the strangest parts, because the answer to why is in the diff and the linked ticket. The production configuration, because a branch that is unreachable with the current flag values is not part of the system in any meaningful sense. The telemetry, because the volume through each path tells you where the system actually lives, and a code path with no traffic in ninety days is a different object from one carrying most of the load. And the people, because the two questions worth spending someone's time on are what surprised you when you worked on this and what breaks most often.
What you should not use it for is deciding what to change. The output of two weeks is a shared understanding, not a plan, and any restructuring proposal produced from source alone will be generic in exactly the ways your system is not. What you should also do, and candidates forget, is write the understanding down in a form that survives you: a diagram, a decision record capturing the invariants you found, and above all a set of characterisation tests, because those are the only form of documentation that cannot rot silently.
Ask a model to explain a thousand-line function and it gives you a fluent summary. How much do you trust it, and what do you do next?
Trust it as a hypothesis with a reasonable prior and a specific bias. The bias is towards describing the code as written rather than as executed, so every branch gets narrated as though the system takes it, and the parts that are dead are described with the same confidence as the parts that carry all the traffic.
So the next step is to make it falsifiable and then falsify it. Re-ask for structured claims rather than prose: the preconditions, the invariants, every early return and the condition that triggers it, every external call, every case that throws. Check a sample against the source. Then get runtime evidence for the branch question, which is the one the source cannot answer: check the configuration values in each environment, look at the telemetry for the paths, and if neither exists, add a counter and wait a week. A function where three of five branches have taken zero traffic in a month is a much smaller function than it appeared, and knowing that changes what you do next more than any amount of summary.
The second prompt worth running is the adversarial one. A request to explain produces explanation; a request to identify what looks inconsistent, unreachable, or contradictory produces a different and usually more valuable list, because it surfaces the half-finished refactor and the two comments that disagree.
How would you assemble the right context from a repository that will never fit in a window?
Treat it as retrieval with a structural advantage that a document corpus does not have. Start from a seed — the file, the symbol, the failing test, the stack trace — and expand along relationships you can compute exactly rather than approximate: the definitions of the symbols referenced, the callers of the function being changed, the implementations of the interfaces involved, the tests that exercise it. That call-graph neighbourhood is a much better context set than the nearest neighbours by embedding, because it is the set that actually determines behaviour.
Then layer the approximate methods on top for what the exact ones cannot reach. Lexical search finds error strings, configuration keys and identifiers when you know the name. Semantic search finds the module that does something conceptually similar under a different vocabulary, which is how you locate the existing pattern you should be following. Directory proximity gets you the conventions of the area.
Two additions distinguish a good answer. Include history for the lines being touched, because why is the question and history is where why lives. And be explicit about what none of it retrieves: runtime wiring through dependency injection, reflection, configuration-driven dispatch, and anything a framework resolves at startup. Those are exactly the edges a static traversal cannot follow, they are common in old enterprise code, and a context set assembled without acknowledging them will look complete and be missing the caller that matters.
Why is retrieval over code harder than retrieval over documents?
Because the unit of meaning is not the unit of text. A paragraph about a refund policy carries its own meaning; a function does not. What it does depends on its callers' expectations, the contract of the interface it implements, the invariants its class maintains, and frequently on a subclass elsewhere overriding the branch you are reading. Chunk by size and you sever those relationships precisely where they carry the information.
There is also a vocabulary problem in both directions. Code is written in identifiers that are often abbreviations, often historical, and often actively misleading, so the function that computes tax may be called applyAdjustments and share no words with a query about tax. And the same words recur everywhere: process, handler, update, data appear in ten thousand places and discriminate nothing.
The compensating advantage is the structure, which is why the honest answer is that retrieval over code is harder in the ways that embeddings address and easier in the ways a parser addresses. A system that uses only one of the two is leaving the other's strengths on the floor.
Tests and verification
You need to change a class with no tests, in a system where a mistake means a customer-visible incident. Walk me through it.
Nothing gets changed until there is something that will notice if the behaviour moves. So the first work is characterisation: pick inputs that reach each path, run the current code, record what it returns, and pin it. Not what it should return — what it does return, including the cases where the current behaviour is clearly a bug. The test is a tripwire, not a specification.
This is the volume-limited, low-judgement work where assistance is most useful, and it is worth saying why the safety profile is good: you are not trusting the model's idea of correctness, because you are not asserting correctness. The model proposes inputs and writes scaffolding, the running system supplies the expected values, and the risk that remains is a test that cannot fail. Guard against that by mutating the implementation and confirming the suite goes red before you rely on it.
Annotate as you go. Each pinned behaviour gets a note saying whether it looks intentional, looks accidental, or is unknown. That annotation is what stops the next engineer treating an accident as a contract, and it is the part no tool produces for you.
Then make the change, with the suite as the tripwire. Where the blast radius justifies it, add output comparison against real traffic: run both paths, compare, and explain every difference rather than tolerating any. Explained differences are fine; unexplained ones are the entire point of the exercise.
Why write tests that pin behaviour you think is wrong?
Because the alternative is changing two things at once and not knowing which one broke. A characterisation test separates "did I preserve the system" from "is the system correct", and those are different questions with different owners. Preservation is yours to guarantee. Correctness may not be yours to decide, because the behaviour you think is wrong may be one a customer has integrated against for four years, and fixing it is a product decision with a communication plan attached.
There is a diagnostic benefit too. A characterisation suite is a map of what the system does, including the parts nobody documented, and the act of building it surfaces the behaviours that are surprising. Several of them will turn out to be genuine defects nobody had noticed, and finding them is a side effect worth having.
The practical rule is that pinning is not endorsing, and the note in the test is what makes that distinction survive contact with the next reader.
A generated test passes on the first run. What does that tell you?
Almost nothing, and treating it as a green light is one of the most common mistakes in this area. A test that has never failed has not been shown to be capable of failing, and there are several ways to write one that cannot. It may assert something trivially true. It may exercise mocks so completely that no production code runs. It may assert on a value the test itself computed by the same logic as the implementation. It may be testing a path that the arguments never reach.
So the confirmation step is mandatory: change the implementation in a way that should break the assertion, confirm the test goes red, revert. Do it for every generated test if the suite is small, and by sampling plus a mutation check if it is large. This is the same instinct as deciding whether a failure is a bug or a bad test, applied in advance rather than after the fact.
The related point is that a large volume of generated tests will move the coverage number a great deal and may move the assurance very little, because coverage records execution and says nothing about assertion. If somebody presents a coverage jump as evidence that the legacy risk has been addressed, the question to ask is how many of those tests have been observed failing.
How do you catch a behavioural regression that no test covers and nobody documented?
You accept that you cannot catch it by reading, and you build the comparison instead. Before the change, capture what the current system does over a representative set of inputs — from production traffic if you can shape it, from recorded requests if you have them, from generated inputs across the domain if you have neither. After the change, run the same set and diff.
For high-stakes paths, do it against live traffic rather than a captured sample: run both implementations, serve the old one, log the differences. This costs real effort and needs a plan for what to do with the differences, and part of the judgement is knowing that it is warranted when the consumers are external, the surface is wide, or the behaviour touches money, and not warranted for an internal tool with three users.
The honest caveat to offer is that this catches differences on the traffic you observed and nothing else. The behaviour depended on by a report that runs once a quarter, or by a customer script that fires only at month end, is invisible to a week of comparison. That is why the population of consumers matters as much as the sample: knowing who reads this output is part of the change, and the ones you cannot enumerate are the reason a rollback plan exists.
Would you let a model choose what to test?
For enumerating inputs, yes, and it is good at it — reaching branches, finding boundary values, proposing the null and empty and negative cases a tired human skips. That enumeration is genuinely valuable and frequently surfaces cases the author had not considered.
For deciding what matters, no, because that is a risk judgement about the system and the business rather than a property of the code. Which paths carry money, which are customer-visible, which have failed before, which have an external consumer you cannot roll back — none of that is in the source. The framing to reach for is the same as in how you decide what to automate: the value of a test is set by the cost of the failure it prevents, and the cost of a failure is not a code-level fact.
The synthesis is the answer that lands: let it propose broadly, then apply your own prioritisation, and be willing to discard most of what it proposed. A hundred generated tests of which you keep thirty is a good outcome; a hundred of which you keep a hundred means nobody prioritised.
Refactoring and migration
Where is the line between a refactor you would hand to a model and one you would not?
Whether the transformation has a rule or a reason. A rule is uniform, statable in one sentence, and checkable — rename this concept everywhere, replace this deprecated call with its successor, add this parameter to every implementation. Those transfer well, and the reviewer's job is to review the rule and sample the instances rather than to read every occurrence.
A reason is not in the code. Splitting a module because two teams collide in it, merging two because the boundary tracked an org chart that no longer exists, leaving something ugly alone because it is stable and about to be deleted — these depend on where the organisation is going and what is about to be replaced. Ask for them and you will get the shape most commonly recommended for that language, which is defensible in general and unlikely to be right here.
Add the qualification that where a deterministic tool exists you should prefer it. A language's own rename or an AST codemod is exact and reviewable as a single rule, which is strictly better than two hundred individually plausible edits. The model's place is writing the codemod, or handling the residue the codemod could not express — and that residue is where the review attention goes, because it is precisely the part that required judgement.
You are upgrading a major framework version across a large application. What does assistance change and what does it not?
It changes the mechanical sweep, which is real and tedious: renamed packages, altered signatures, moved configuration, replaced annotations. Fed the migration guide, a model handles that competently and removes days of work.
It changes nothing about the class of problem that will actually hurt you, which is the semantic change that compiles. A default that flipped. A serialiser that now emits a field it used to omit. A pool whose eviction behaviour differs. A parser that got stricter about a format you happen to produce. Transaction or ordering semantics that shifted. These build cleanly, pass a thin suite, and surface under load or at a boundary condition weeks later. They live in release notes as one line among two hundred, and summarisation compresses them away precisely because they are individually small.
So the plan is: characterisation tests first, over the behaviours the framework touches, because that is what gives you a tripwire. Mechanical sweep with assistance. Then a manual pass over the release notes treated as a checklist, where each behavioural change is checked against your own usage rather than accepted as generally applicable. Then staged rollout, because some of these only appear under real traffic. The tests are the enabling artefact and the sweep is the convenience, and getting that ordering the right way round is the answer.
A model produced a four-hundred-line change across eleven files. How do you review it?
The first answer is that you probably do not, and that saying so is the correct response rather than an evasion. If the change is not uniform, ask for it to be split, because there is no honest way to review four hundred lines of distinct judgement in one sitting and approving it anyway is worse than rejecting it.
If it is uniform — one rule applied everywhere — then review the rule, sample the instances, and require the diff to be reproducible so that "I regenerated it and got the same output" is available as evidence. Concentrate the remaining attention on the instances that deviate from the rule, because those are where the transformation needed judgement and judgement is where it went wrong.
Either way, everything mechanically checkable happens before you look: build, types, lint, existing suite, security and licence scanning. Then allocate your attention by risk rather than evenly — the boilerplate gets a glance, and the lines touching a boundary, a transaction, a permission check or a money calculation get the budget. And the submitter is expected to have understood it and to point you at the parts that matter; if they cannot explain a non-obvious decision without re-reading, the change is not ready.
Would you use a model to delete dead code?
To find candidates, yes. To decide, no, and this is the question where a confident wrong answer is most common because deletion feels like the safest possible operation.
Reachability in a large system is not a static property. Reflection resolves class names from configuration. Dependency injection wires implementations chosen at startup. Serialisation frameworks call constructors and setters nothing references. Scheduled jobs and message consumers have no in-code caller. Feature flags gate paths that are dark today and will be enabled next quarter. Endpoints have external consumers who never told you. Every one of those defeats static reasoning, and a model reasoning over source will report the code as unreferenced with complete confidence, because by the evidence it has, it is.
So the process is: static analysis proposes, runtime evidence decides. Instrument the code path and observe for a period long enough to include the slow cycles — a month at least, a quarter if anything in the system runs quarterly. Search configuration and data for the class or endpoint name as a string, because that is how reflection references it. Check whether anything outside the repository calls it. Then remove it behind a switch or as a revertible commit rather than as a deletion, so that the recovery when you are wrong is a minute rather than an archaeology exercise.
Failure modes
What is the most dangerous way generated code fails on an existing codebase?
Silently changing behaviour that nothing asserts and nobody documented. Everything else announces itself: an invented method fails the build, a wrong contract usually fails a test, an overreaching diff is visible in review. A behavioural change in an unasserted case produces green everywhere and appears weeks later as a customer report, by which time the change is one of four hundred commits and the connection is not obvious.
The mechanism is that long-lived systems accumulate behaviour nobody chose. Ordering that falls out of an index. An empty list where an error would have been more principled. A rounding direction that came from a type conversion. A timezone that came from the server. None of it was designed, all of it is depended on, and a refactor that is better by every reasonable measure can change any of it.
Which is why the answer loops back to characterisation tests written before the change, and to output comparison where the stakes justify it. It is also why "the tests passed" is not a statement about safety on a codebase whose tests were never comprehensive, and treating it as one is the specific error the question is looking for.
A model produced code that follows every convention in the file and is exactly what you are trying to stop doing. How does that happen and what do you do?
It happens because inferring conventions from surrounding context is the mechanism, and a legacy codebase's conventions include its mistakes. If the module catches broad exceptions and logs at debug, if it concatenates query strings, if it uses the home-grown result type everyone agreed to retire, then that is what the majority of the neighbouring code demonstrates and that is what gets reproduced. The output is locally correct and directionally wrong, and no automated check catches it because by every mechanical measure it is fine.
The mitigations are about making intent explicit rather than implicit. Say the target pattern in the instructions instead of hoping it is inferred. Point the context at the module that already does it the new way rather than the one you are editing. Where the rule can be expressed as a lint check, write the lint check, because a convention with enforcement is a different object from a convention in a wiki. And keep the architectural decisions somewhere the tooling can be given them.
The point that elevates this answer is the compounding one. Generated code follows the majority pattern; if generated code is a growing share of commits, the majority pattern grows. A codebase stuck halfway between two conventions will now drift backwards, invisibly, because every individual change is locally reasonable. The structural fix is to finish migrations rather than parking them at half done, which is advice that was always true and now has a mechanism pushing it.
Why is a hallucinated API more dangerous in a codebase you do not know?
Because your defence against it was recognition, and recognition requires familiarity. On a framework you know, an invented method reads wrong on sight. On an internal service you have never used, an invented method and a real one are indistinguishable, so you have no filter and are relying entirely on the toolchain.
That is survivable when the toolchain is strong. In a statically typed language with a good build, an invented symbol fails immediately. In a dynamic language, it fails when the branch executes, which may be in production, on the error path, at three in the morning.
The genuinely dangerous variant is not invention at all. It is a real function used against a contract it does not honour: the method exists, it compiles, and it must not be called during the batch window, or it assumes a lock the caller does not hold, or it returns a stale figure when the cache is cold. Those constraints are in a comment on another class or in nobody's head at all. No static check exists. The only defence is a human who knows the subsystem being in the path of the review, which is an argument for code ownership on sensitive modules rather than for a better tool.
Your team ships more code and the incident rate goes up. How do you find out whether the tooling is responsible?
Refuse the correlation as evidence, then go and look at the incidents individually, because the aggregate cannot answer this and a classification can. For each one, ask what the change was, whether the person who submitted it understood it, whether it was reviewed properly, and what would have caught it. Then look for a pattern in those answers rather than in the count.
The patterns that would implicate the tooling are specific and different from each other. Incidents concentrated in unfamiliar code suggest the problem is context and ownership. Incidents from large diffs that were approved quickly suggest review capacity is the binding constraint. Incidents from behavioural changes in untested areas suggest the verification gap rather than the generation. Each of those has a different fix — module ownership, a size limit, a characterisation-test push — and prescribing before classifying is how a team ends up with a policy that addresses none of them.
Be honest about confounders too. More code shipped means more opportunities regardless of how it was written, and adoption is not random across teams or across kinds of work. What you can say with confidence is whether the defects share a mechanism, and if they do, that is far more actionable than any rate.
Process, constraints and judgement
Who is accountable for a generated change that causes an incident?
The person who submitted it, without qualification. Every organisation that has thought about this has arrived at the same place, because the alternative distributes responsibility to something that cannot hold it. Submitting a change is a claim that you understand it and believe it is correct, and how it was produced does not alter the claim.
The useful follow-through is what that implies operationally. It implies you cannot submit what you do not understand, which is a real constraint on how large a change you should generate. It implies the review record is the evidence of human judgement, which matters when an auditor asks. And it implies the postmortem asks why the process let it through rather than which tool produced it, because a tool that produced a plausible wrong change did what it does, and the interesting question is what should have caught it.
On whether to label generated code in the commit or the pull request, both positions are defensible and the trade-off is what is being scored. Labelling helps a reviewer calibrate and helps later analysis of where defects come from; it also risks becoming ritual, and risks a reviewer treating an unlabelled change as pre-verified. What is not defensible is using the label to shift responsibility.
Can you send this codebase to a model? Talk me through the decision.
Establish first that the question is per-repository rather than per-company, because that is the framing most people miss. Different repositories carry different sensitivity, and a sane organisation ends up with tiers: some code can go to a hosted service under an agreement, some can only go to a deployment inside the network boundary, and some cannot leave at all.
Then audit what is in the repository besides code, which is the part that catches teams out. Test fixtures with real customer records. Configuration with credentials and internal hostnames. Comments naming the customer who demanded a workaround. Data files nobody remembers committing. An approval that considered only source has not considered the repository.
Then the provider questions: where processing happens, what is retained and for how long, whether prompts train anything, whether the contracted tier differs from the default one, and which subprocessors are involved. And the output side: licence obligations on distinctive generated blocks, which means running the same licence scanning over first-party source that you already run over dependencies.
Close with the position you would take, because an interviewer wants to know you can operate rather than only object. Naming the questions, proposing a tiering, and getting the highest-value low-sensitivity repositories moving while the harder ones are resolved is a better answer than either a blanket yes or a blanket no.
Your organisation forbids sending source code to an external service. What can you still do?
Quite a lot, and knowing that is the signal. Models deployed inside your own boundary remove the egress question entirely and change the capability trade-off rather than eliminating it, and the residual work is capacity planning and operations rather than legal.
Where even that is unavailable, several of the highest-value activities never needed a model in the first place. The characterisation-test push is a discipline, not a tool. Structural analysis, call graphs, dead-code candidates and dependency mapping come from a parser. Test-data generation and property-based testing are ordinary techniques.
And you can often use assistance on the parts that are not proprietary: reasoning about a public framework's behaviour, drafting a migration plan described in the abstract, explaining a standard library's semantics. The skill is decomposing the problem so the sensitive part stays inside. What you should not do is quietly route around the policy, and saying so plainly is worth more in an interview than any technical answer, because the interviewer is partly checking whether you would.
How would you decide whether adopting this tooling helped your team?
Decide what would count as evidence before looking, and refuse the measures that will move regardless. Suggestion counts, lines generated and diff volume all rise when the tool is present and say nothing about outcomes. Self-reported perception is worth collecting as an experience signal and is not evidence of throughput, because it is confounded by novelty. And adopters select themselves, so comparing them to non-adopters compares people rather than practices.
What is left, on brownfield specifically, is narrower and more honest. Whether work that used to be refused now happens is the strongest available signal, because it is a categorical change rather than a rate: a subsystem nobody would touch now has tests and is being modified, and that is directly attributable. Alongside it, watch the risk side — whether incidents traceable to changes in unfamiliar code went up, whether the lag between a change landing and its defect surfacing lengthened, and whether review is still catching what it used to.
The answer that reads as senior includes what you would not claim. Anyone presenting a clean percentage improvement in delivery from a tool adoption has measured something other than what they think, and being willing to say the effect is real but not cleanly separable is more credible than a number.
Tell me about a time an AI-assisted change went wrong.
Answer with a mechanism, not a caveat. The structure that works is: what you asked for, what came back, why it looked right, what caught it or failed to, and what you changed about how you work afterwards. The last part is what the question is for — an incident with no adaptation attached is a story, and an incident with an adaptation is evidence of a working feedback loop.
Do not reach for the largest failure you can find. A small, specific one told precisely beats a dramatic one told vaguely, because specificity is the thing that cannot be faked and vagueness is what an interviewer probes. If your honest answer is that nothing has gone badly wrong yet, say that and say what you watch for and why, rather than inventing something; a fabricated incident collapses on the second follow-up and takes the rest of your credibility with it.
How do you talk about your experience with this if you have used it for six months on side projects and not much at work?
Say exactly that, and then be precise about what transfers and what does not. Six months of real use is real experience, and the honest framing is that it was in conditions that differ from the ones the role involves: small codebases, code you wrote, tests you controlled, no data constraints, no review burden on anyone else. Naming those differences yourself demonstrates the calibration the interviewer is testing for far better than any claim about volume.
Then go one step further and say what you would expect to be different here, because that turns a limitation into a demonstration of understanding: that you would expect context assembly to be the binding problem, that you would expect the absence of tests to be the constraint rather than generation speed, and that you would want to know the data policy before starting. An interviewer hearing that has learnt that you understand the subject even though you have not lived all of it.
The failure mode to avoid is padding. Claiming a scale of experience you do not have collapses on the specifics, and interviewers ask the specifics deliberately: which subsystem, what did it get wrong, what did you do about it. Being caught overstating one thing causes everything else to be discounted, which is a much larger loss than the modest experience would have cost you.
When would you tell a team to stop using it for something?
When a class of work has produced repeated failures with the same mechanism and the mechanism has no cheap control. That is the test, and phrasing it as a test rather than a list is what makes the answer general.
Concretely, the strongest candidates are changes where verification is impossible and the blast radius is wide — a data migration that runs once and cannot be reversed, a permission check where a wrong result is a breach rather than a bug, a money calculation in a path with no comparison harness. In each of those the loop the tooling depends on is broken: there is no test to feed the failure back into, and the first observation of the failure is the incident.
The framing that earns credit is that this is a scoping decision rather than a verdict. You are not banning a tool; you are saying that in this area the verification is not good enough to support generated change at speed, and that the fix is usually to build the verification rather than to argue about the tool. Once the comparison harness exists, the constraint can be revisited. Stating that keeps you on the side of improving the system rather than adjudicating it.
How to prepare
Get one real story with a mechanism in it. A change that went wrong, what made it look right, what caught it, and what you altered afterwards. Almost every question in this subject can be answered from a concrete experience, and almost none can be answered convincingly from principle alone, because the principles are widely published and the specifics are not.
Learn the characterisation-test argument properly, because it is the highest-leverage thing to know here and the one most candidates get half right. Tests before the change, pinning current behaviour including behaviour you think is wrong, the running system as the oracle, a note on each recording whether the behaviour is intentional or accidental, and a confirmation that each test can fail. If you can only prepare one thing, prepare this.
Be able to enumerate the failure modes and attach a distinct detection mechanism to each, because the difference between "sometimes it is wrong" and a differentiated list is the difference between having used it and having thought about it.
Know your own boundary. What you would not send, what you would not generate, what you would not approve, and why in each case. A candidate with no limits reads as someone who has not encountered the situations that produce them.
And be honest about the extent of your experience. Overclaiming is the failure mode this particular subject invites, because the vocabulary is easy to acquire and the practice is not, and interviewers have calibrated their follow-ups accordingly. Saying what you have done, what you have not, and what you would expect to be different in their environment is a stronger position than any inflated version of it.