Halo: Campaign Evolved arrived this month as a ground-up remake of the 2001 Xbox launch title, rebuilt in Unreal Engine 5. Which is a fascinating engineering decision, because the studio had a working game that people demonstrably still enjoyed, and chose to rebuild it in a different engine anyway — while keeping the level design, the encounter pacing and the feel that made it worth remaking.
That is precisely the question senior engineering interviews ask, usually as: "here's a legacy service nobody wants to touch — what do you do?" It is not a technical question with a correct answer. It is a judgment question, and it is designed to find out whether you have ever actually lived through a rewrite. This guide covers the framework, the failure modes, and how to answer it well.
Why interviewers love this question
Because the naive answer is instant and wrong. Almost every engineer's instinct on meeting bad code is to rebuild it, and almost every experienced engineer has watched a rewrite consume eighteen months and ship something with fewer features and new bugs.
So the question separates three groups: candidates who say "rewrite it" immediately, candidates who say "never rewrite" as a memorised rule, and candidates who ask what problem the code is actually causing. Only the third group is answering the question.
Start by refusing the premise
The strongest opening is not an answer, it is a question: what problem is the current system causing? "It's old" is not a problem. "It's ugly" is not a problem. Real problems sound like:
- Change velocity has collapsed — a two-line feature takes three weeks.
- It fails in production regularly and nobody can predict how.
- It cannot meet a load or latency requirement that the business now has.
- It depends on something being decommissioned — an unsupported runtime, a database version, a vendor shutting down.
- Nobody who understands it still works here, and onboarding takes six months.
- It is a security liability that cannot be patched.
Note which of those justify a rewrite: mostly the ones about capability, not aesthetics. Halo's remake had a real forcing function — a new engine, new platforms, new visual expectations — not "the old code was messy." Being able to distinguish a forcing function from an irritation is the core of this answer.
The case for refactoring in place
The default should be incremental improvement, and you should be able to say why with specifics:
- The system works. However ugly, it encodes years of accumulated behaviour — edge cases, client quirks, regulatory rules, bug fixes for problems you have never heard of. Joel Spolsky's well-known argument about rewrites is essentially this: that ugly code is often ugly because it is correct.
- Value ships continuously rather than in one distant cutover, so the business is never asked to fund eighteen months of silence.
- Risk is bounded. Each change is small and revertible.
- You learn the domain as you go — and the reason most rewrites fail is that the team did not understand the behaviour they were replacing until they had already replaced it.
The mechanics worth naming: get tests around the current behaviour first (characterisation tests that assert what it does, not what it should do), then improve behind that safety net. Michael Feathers' Working Effectively with Legacy Code is the standard reference and naming it signals you have done this.
When a rewrite is genuinely the right call
Do not answer this question as though rewrites are always wrong — that is its own memorised rule, and interviewers push on it. Rewrites are justified when:
- The architecture cannot meet a real requirement. A single-tenant design that must become multi-tenant, or a synchronous system that must become event-driven, may not be reachable incrementally.
- The platform is dying. An unsupported language runtime or a vendor exit is a forcing function with a date attached.
- The scope is small enough to be honest about. Rewriting one service is a project; rewriting a platform is a multi-year bet.
- The behaviour is well understood and documented — this is the condition people skip, and it is the one that most determines success.
- You can run both systems in parallel and compare, rather than doing a big-bang cutover.
The strangler fig, and why it is the answer they want
The best answer to "rewrite or refactor" is usually neither, exactly — it is incremental replacement. The pattern has a name interviewers recognise: the strangler fig, from Martin Fowler, after the plant that grows around a tree and gradually replaces it.
Concretely: put a facade or proxy in front of the legacy system, build new functionality behind it in the new implementation, route traffic piece by piece, and retire the old parts as they are replaced. You get the architectural freedom of a rewrite with the incremental risk profile of a refactor.
The techniques to name alongside it:
- Parallel run (dark launch) — send production traffic to both systems, serve the old result, compare the new one, and fix discrepancies until they stop. This is how you discover the behaviour nobody documented.
- Feature flags for gradual rollout and instant rollback.
- Branch by abstraction — introduce an interface over the thing you are replacing so both implementations can coexist in trunk.
- Migrate the data last, or not at all initially — data migration is usually the hardest and least reversible part.
Halo's remake is instructive here too: it kept level layouts and encounter design — the behaviour that mattered — while replacing the engine underneath. That is close to a strangler fig in spirit: replace the implementation, preserve the observable behaviour that users actually valued. Our system design guide and microservices guide cover the architectural context.
The failure modes to name
Mentioning these unprompted is what marks a senior answer, because they are what actually goes wrong:
- The second-system effect — the replacement accumulates every feature anyone ever wanted, and becomes more complex than the thing it replaced.
- The feature freeze that isn't. The business keeps changing the old system while you rebuild it, so you are chasing a moving target and the rewrite never converges.
- Underestimating undocumented behaviour. The old system does things nobody remembers implementing, and those things have users.
- The last 20% takes 80% of the time — the obscure edge cases, the one client integration, the reporting nobody mentioned.
- Losing the people who understood the original mid-project.
- No rollback plan. A big-bang cutover with no way back is the version that ends up in a postmortem.
And the honest organisational point, which lands well: a rewrite is as much a political decision as a technical one, because it asks the business to fund a long period with no visible new value. That is why incremental approaches usually win — not because they are technically superior in every case, but because they survive contact with quarterly planning.
How to answer it in the room
A structure that works, in order:
- Ask what problem the system is causing, and get specific about cost — engineering time, incidents, blocked revenue.
- Ask whether there is a forcing function with a date: a decommission, a compliance deadline, a platform end-of-life.
- State your default — incremental improvement behind characterisation tests — and say why.
- Name the conditions that would change your mind, which demonstrates you are reasoning rather than reciting.
- Propose the strangler fig as the middle path, with a parallel run to discover undocumented behaviour.
- Say how you would sequence it — the highest-pain, lowest-risk piece first, to build evidence the approach works.
- Name what could go wrong and how you would detect it early.
If you have a real story — a system you improved incrementally, or a rewrite you were part of, including one that went badly — tell it. This question rewards scar tissue more than theory. Our tell me about a time you failed guide covers the structure for the version where it went wrong.
The books, real postmortems, ChatGPT — where each fits
- Working Effectively with Legacy Code (Michael Feathers) — the reference for characterisation tests and seams. Name it.
- Martin Fowler's writing on strangler fig and branch by abstraction — short, free, and the exact vocabulary interviewers use.
- Joel Spolsky's "Things You Should Never Do" — the canonical argument against big rewrites; worth reading so you can agree with it partially rather than quoting it as law.
- Public migration postmortems — engineering blogs describing multi-year migrations, including the ones that admit what went wrong, are the best source of concrete detail.
- Your own codebase — the highest-yield preparation is having actually put tests around something scary and changed it.
- ChatGPT — good for generating a migration plan outline. It will happily agree that you should rewrite it, which is the failure mode of asking a system that has never maintained anything.
- Greenroom — the spoken layer. Ari, the AI interviewer, runs the judgment rounds out loud and pushes when your answer is a rule rather than a decision. Fair tradeoff: Ari has not seen your legacy system.
How to prepare for engineering judgment questions
- Week 1: read Feathers on seams and characterisation tests, and Fowler on strangler fig. Explain both aloud in ninety seconds.
- Week 2: take three systems you know and, for each, argue both sides — the case to rewrite and the case to improve in place.
- Week 3: write up one real migration or improvement you were part of, including what went wrong and what you would sequence differently.
- Final week: two spoken mocks where the interviewer changes a constraint mid-answer — a compliance deadline appears, or the team halves.
Also useful: our machine coding round guide covers design judgment under time pressure, the senior engineer guide covers the wider seniority signals, and the design patterns guide covers the vocabulary.
Frequently asked questions
How do you answer the rewrite versus refactor interview question?
Do not answer immediately. Ask what problem the current system is actually causing and quantify it — engineering time lost, incidents, blocked revenue — and ask whether a forcing function exists with a date attached, such as a platform decommission or compliance deadline. Then state incremental improvement behind characterisation tests as your default, name the conditions that would change your mind, and propose the strangler fig pattern as the middle path.
When is rewriting a system actually the right decision?
When the existing architecture genuinely cannot meet a real requirement, such as a single-tenant design that must become multi-tenant; when the platform is dying through an unsupported runtime or vendor exit, which supplies a hard date; when the scope is small enough to be honest about, meaning one service rather than a platform; when the existing behaviour is well understood and documented; and when you can run both systems in parallel rather than attempting a big-bang cutover.
What is the strangler fig pattern?
Named by Martin Fowler after the plant that grows around a tree and gradually replaces it, the strangler fig pattern puts a facade or proxy in front of a legacy system, builds new functionality behind it in the new implementation, routes traffic piece by piece, and retires old components as they are replaced. It gives you the architectural freedom of a rewrite with the incremental, revertible risk profile of a refactor.
Why do most large rewrites fail?
The dominant cause is undocumented behaviour: the old system does things nobody remembers implementing, and those things have users. Other common causes are the second-system effect where the replacement accumulates every feature anyone ever wanted, a feature freeze that never actually holds so you chase a moving target, the final twenty percent of edge cases consuming most of the timeline, losing the people who understood the original, and having no rollback plan.
What is a characterisation test?
A characterisation test asserts what a system currently does rather than what it should do, capturing existing behaviour — including its bugs and quirks — so you can change the implementation underneath without altering observable behaviour. It is the standard first step when improving legacy code you do not fully understand, because it converts unknown behaviour into a safety net. The technique comes from Michael Feathers' Working Effectively with Legacy Code.
What is a parallel run or dark launch during a migration?
A parallel run sends real production traffic to both the old and new implementations, serves the result from the old system, and compares the new system's output against it, logging discrepancies. It is the most reliable way to discover undocumented behaviour, because differences surface against real traffic patterns rather than the cases you thought to test. You cut over only once the discrepancy rate has fallen to zero and stayed there.