Skip to content
Preptima

Working With Legacy Systems: A Masterclass on Modernisation Interviews

A complete treatment of the legacy modernisation interview for engineers and architects: what makes a system legacy, characterisation tests and seams, the strangler fig and where it fails, rewrite versus incremental replacement, data migration and cutover, parallel running and reconciliation, anti-corruption layers, sequencing a multi-year programme, and the signal an interviewer grades behind every question.

Masterclass·98 min read

What it is

A legacy system is one you cannot change safely. That is the whole definition, and every useful thing in this subject follows from taking it literally rather than treating it as a slogan. Age is not the criterion. A twelve-year-old payments engine with a comprehensive test suite, a two-minute build, an owner who answers the phone and a deployment that happens on Tuesdays is not legacy in any sense that matters, however unfashionable its stack. A React front end written eighteen months ago by contractors who have gone, with no tests, three state-management libraries and a build nobody can reproduce, is legacy on its first birthday. What separates them is not the year on the copyright header, it is whether a competent engineer can make a required change and know, before it reaches customers, whether they have broken something.

Interviewers ask about legacy because it is where the majority of professional engineering actually happens and because it is the cleanest available test of judgement under incomplete knowledge. A greenfield design question lets a candidate assume away every inconvenient fact. A legacy question does not, because the inconvenient facts are the question. Somebody built this, they are gone, the documentation describes a system that may never have existed, the behaviour customers depend on is partly undocumented and partly accidental, the business will not stop while you work, and you are being asked what to do on Monday. There is no answer that is simply correct. There are answers that show you understand what you do not know and answers that show you do not.

flowchart TD
    accDescr: The incremental replacement loop from understanding and characterising, through establishing seams and tests, carving one slice and running old and new in parallel, to a reconciliation check that sends mismatches back to the slice and matches on to cutover and decommissioning of the old path before the next slice, with dashed arrows showing legacy still serving throughout the parallel run and the cutover.
    A[Understand and characterise] --> B[Establish seams and tests]
    B --> C[Carve one slice]
    C --> D[Run old and new in parallel]
    D --> E{Reconciles}
    E -->|No| C
    E -->|Yes| F[Cut the slice over]
    F --> G[Decommission the old path]
    G -->|Next slice| C
    L[Legacy keeps serving throughout] -.-> D
    L -.-> F

Look at the decommission step, because that is the one programmes skip: a slice that is cut over but never retired leaves both implementations alive and both needing maintenance, and an organisation that skips it four or five times in a row has not modernised anything, it has bought a second system to run alongside the first one forever.

What being unable to change safely is made of

Say "cannot change safely" out loud in an interview and a good interviewer will ask you what stops you, because the answer determines the whole plan and the causes are not interchangeable. There are roughly five, and a real system usually has three of them.

The first is the absence of a feedback loop. There are no tests, or the tests are integration tests against an environment that has drifted, so the only way to learn whether a change is correct is to release it and wait for complaints. Everything else in modernisation is downstream of this, because a plan that involves changing the system is a plan that requires a way to know whether the change was right.

The second is the absence of a deployment path. The release involves a change window, a runbook, a database script somebody runs by hand, and a person. When a release costs a weekend, changes get batched, batches get large, large changes fail more often, failures reinforce the fear, and the release cadence falls further. This is a self-tightening loop and it is why what ships without passing the deploy pipeline is a legacy question wearing a DevOps question's clothes.

The third is the absence of comprehension. The code is present and readable and nobody can predict the consequence of a change, because the coupling is implicit: a shared table that six modules write to, a configuration flag whose meaning changed twice, a stored procedure that another system calls without anybody's knowledge, a nightly job that quietly repairs the data the daytime path corrupts. Comprehension failure is not fixed by reading harder. It is fixed by instrumentation, by tracing what actually runs, and by writing down what you learn where the next person will find it.

The fourth is the absence of a supported substrate. The runtime is out of support, the library has no maintainer, the vendor has announced an end-of-life date, or the hardware has one supplier. This one has a clock on it that is not yours, which changes the shape of the argument entirely, since you are no longer choosing when to act.

The fifth is the absence of a person. One engineer understands it. Everything routes through them, they cannot take a holiday without a rota being rearranged, and their departure would be a business continuity event. This is a legitimate architectural risk even though it appears on no diagram, and naming it is one of the reliable senior signals in this subject.

The shapes the question arrives in

Concept questions check that your vocabulary is load-bearing rather than decorative: what a seam is, what a characterisation test asserts, what the strangler fig pattern actually requires you to have, what an anti-corruption layer protects and from what. These are quick to ask and quick to fail, because a candidate who defines the strangler fig as "replace it bit by bit" has told the interviewer they have read a blog post.

Scenario questions put you inside a mess and watch the order of your moves. You inherit a system nobody understands. The vendor has announced end-of-life in fourteen months. The migration ran overnight and the reconciliation does not balance. The new service disagrees with the old one on nine hundred records out of four million and the cutover is on Friday. What interviewers grade here is sequence, not solution, and the most common failure is proposing the right actions in the wrong order.

Design questions ask you to construct something whose correctness has to hold while a live system keeps running: a dual-write path, a reconciliation harness, a routing layer that can send a percentage of traffic to a new implementation and send it back, a migration for a table too large to lock. These are graded on the rollback story far more than on the happy path.

Judgement and case-study questions put you between people. The business wants a rewrite because the vendor's sales engineer showed them a slide. The team wants a rewrite because they are bored. The CFO wants to know why five million pounds buys a system that does exactly what the current one does. Your programme is eighteen months in with nothing decommissioned. The honest answer to a modernisation question is sometimes "do nothing yet", and being able to defend that without sounding like you are avoiding work is a distinct and heavily graded skill.

Why we need it

General engineering judgement gives the wrong answer in legacy work often enough to be dangerous, and, as in most domains with that property, the wrong answers are the confident ones.

Consider the instinct to clean up as you go. In a codebase you own, with tests, opportunistic refactoring is close to free and is good practice. In an untested legacy codebase it is the single most common way to cause an outage, because the behaviour you tidied away was load-bearing for a caller you did not know existed and the system has no way of telling you. The discipline that replaces it is unglamorous and specific: pin the behaviour first with a test that asserts what the code does rather than what it should do, then change it. That inversion, from "assert the requirement" to "assert the observed behaviour", is unnatural to engineers trained on test-driven development and is the core technique of the field.

Consider the instinct to delete dead code. Deleting code that nothing calls is correct and valuable. The problem is that "nothing calls it" is a claim about a running system, not about a codebase, and static analysis cannot see a reflective invocation, a database trigger, a scheduled job in a different repository, a partner integration that fires twice a year, or a report the finance team runs at year end. The technique that replaces the instinct is to instrument first and delete second: log a distinctive event at the entry to the suspected-dead path, leave it in production across at least one full business cycle including the quarterly and annual peaks, and delete only what the telemetry never saw.

Consider the instinct to normalise the data model on the way across. A migration is a natural moment to fix the schema, and the fix is usually correct in isolation. It is also the reason migrations slip, because you have coupled a delivery with a hard date to a modelling exercise with no natural end, and every ambiguity in twenty years of accumulated data becomes a decision somebody has to make before the cutover can happen. The judgement being tested is whether you can hold two things at once: that the model is wrong, and that fixing it now converts a migration into a redesign.

Consider the instinct to treat the old system as the enemy. The old system is the specification. It is the only complete, executable, authoritative statement of what the business does, including all the parts nobody can describe, and it is going to keep serving customers throughout your programme. Candidates who talk about the legacy system with contempt tend to write plans that assume it will cooperate.

The knowledge is gone and the behaviour is not

The hardest structural fact about legacy work is the asymmetry between what has been lost and what has been retained. The people who built the system, the reasons for its decisions, the constraints it was designed against, the incidents that shaped it, the customer who insisted on that one exception in 2013 and is still on that contract, all of that has evaporated. What has not evaporated is the behaviour. Every one of those decisions is still executing in production on every request, and customers have built their own processes around the results, including the results that are technically bugs.

This produces the defining risk of the field, which is that a rewrite reproduces the requirements somebody can describe and omits the behaviour nobody can. Both matter, and only one of them is written down. The behaviours that get omitted are, reliably, the ones with the highest cost of omission: the rounding convention in the fee calculation, the ordering guarantee an integration partner depends on, the fact that a blank field means "unknown" in records created before a certain date and "not applicable" after it, the retry that makes a flaky downstream look reliable, the tolerance that lets a mistyped reference match anyway. Each is small. Each is invisible in a requirements document. Each generates a support case, a data correction and a loss of trust when it disappears.

This is why the phase order in modernisation is not negotiable and why interviewers grade it so heavily. Understanding comes before change, characterisation comes before refactoring, seams come before extraction, parallel running comes before cutover, and reconciliation comes before decommissioning. Each phase exists to convert an unknown into a known before you take an action that depends on it, and a plan that skips one is a plan that has decided to find out in production.

The economics are more awkward than the engineering

A modernisation business case is hard to write honestly, and interviewers at senior level know it, which is why they ask. The engineering benefit is real and diffuse: changes become cheaper, incidents become rarer, hiring becomes easier, the failure the organisation is exposed to becomes less likely. The cost is immediate, large and precise. And the deliverable, described plainly, is a system that does what the current one does.

That last sentence is the one you have to be able to survive. A programme sold on "the new system will be better" invites the response that the current one works, and a programme sold on cost savings usually cannot substantiate them, since the running cost of a mainframe or a licensed platform is visible and the running cost of the thing that replaces it is a forecast. The cases that survive contact with a finance function are the ones tied to something the business is already trying to do and currently cannot: a product it cannot launch because the system cannot represent it, a market it cannot enter because the platform is single-currency, a regulatory deadline it cannot meet, an acquisition it cannot integrate, a cost per transaction that will not scale to the volume the sales plan assumes. Modernisation as an enabler of a specific thing the business wants is fundable. Modernisation as hygiene is not, which is the same argument as making the case for platform investment and the reason it recurs.

The second-system effect is a real hazard, not a folk tale

Fred Brooks named the second-system effect in The Mythical Man-Month: the most dangerous system an architect designs is their second one, because it accumulates every idea that had to be left out of the first. The mechanism is straightforward and it applies to any rewrite, whoever is doing it. The team knows exactly what is wrong with the current system, they have been complaining about it for years, and the replacement is where all of those complaints get addressed at once. The result is generalised where it did not need to be, configurable where a constant would do, and abstracted against future requirements that never arrive, while the boring, essential, specific behaviour of the original is still not finished.

The observable form in a modernisation programme is a replacement that is architecturally admirable and functionally incomplete after two years, still not carrying live traffic, while the legacy system continues to accrue changes because the business did not stop. When the two diverge faster than the replacement can close the gap, the programme is dead and only the announcement is outstanding. Recognising this pattern early, and saying that the counter-measure is a hard scope rule that the replacement does exactly what the original does for its first release, is a strong senior signal. The improvements are not cancelled, they are sequenced after the cutover, when they are cheap and reversible instead of being on the critical path of a programme that cannot afford them.

The strategies, and what each actually costs

Modernisation is not one activity, and the most common early error is choosing an approach before establishing which of six the situation calls for. The six are worth holding as a mental checklist, because an interviewer asking "what would you do with this system" is frequently testing whether you consider more than the two you like.

StrategyWhen it is rightWhat it costs you
Leave it aloneIt is stable, rarely changed, on supported infrastructure, and nothing the business wants is blocked by itNothing now. Revisit when a driver appears, and monitor the end-of-life dates
EncapsulateThe internals are unfixable but the interface can be improved, and consumers need a modern contractThe old system survives behind a facade, so its constraints leak and the debt is deferred rather than paid
Replace with a packageThe capability is genuinely undifferentiated, such as payroll, ledger or CRMYour process must bend to the product, integration becomes the project, and you exchange one lock-in for another
Strangle incrementallyThe system is large, business-critical, cannot be paused, and decomposes into slices with clean boundariesA long period of two systems, routing complexity, duplicated data, and the discipline to keep going for years
Rewrite in one moveThe system is small, its behaviour is fully known or externally specified, or the substrate is being switched off and no incremental path existsA hard cutover with no partial rollback and a long period of delivering nothing visible
RetireThe function is obsolete, duplicated elsewhere, or used by so few that the business can absorb the changeThe migration of the remaining users, the data retention obligation, and the political work of telling someone their tool is going

Two rows deserve emphasis because candidates skip them. "Leave it alone" is a legitimate architectural decision and stating it deliberately, with the conditions under which you would revisit, reads as far more senior than proposing work. And "retire" is the highest-return option available in almost every large estate, because the cheapest system to modernise is the one you switch off. Any modernisation assessment that does not begin by asking which of these components can simply be deleted has skipped the free money.

The other thing to notice is that these are not mutually exclusive across an estate. A realistic programme leaves two systems alone, encapsulates one, retires three, buys a package for one, and strangles the one that carries the business. Proposing a single strategy for an entire estate is the mark of someone applying a pattern rather than reading a situation, which is exactly what build versus buy for something that is not your differentiator is set up to expose.

Understanding a system nobody remembers building

The first phase is discovery, and the reason it is hard is not the volume of code, it is that the sources of truth are ranked in an order most engineers get backwards. Documentation describes a system that may once have existed. Code tells you what is possible, not what happens. The running system tells you what happens. Discovery is therefore mostly an exercise in observing production rather than reading a repository, and a candidate who begins by asking for the architecture diagram has begun in the wrong place.

Evidence sourceWhat it tells you that nothing else doesHow to get it in a week
Money and trafficWhich paths carry revenue, so which failures are unacceptableBilling data, request rates on the entry points, the top endpoints by volume and by value
The infrastructure billEvery datastore and always-on component that exists, including the ones nobody mentionedThe cloud invoice or the hosting inventory, read line by line
Incident and support historyThe real failure modes, ranked by frequency, for freeSix months of tickets and the support team's own categories
The last hundred deploysHow fast this system can absorb change, which constrains every plan you can writeThe pipeline history and the release runbook, including the manual steps
Database access logsWho is actually reading and writing, including systems not in your inventoryConnection sources, user accounts, query fingerprints over a full month
Scheduled jobsThe batch layer, which is usually undocumented and usually load-bearingThe crontab, the scheduler console, the job that fails silently every Sunday
A restore rehearsalWhether the organisation's belief about its own resilience is trueRestore one thing into somewhere, end to end, and time it

That last row is worth a sentence on its own. Verifying a restore rather than a backup configuration is frequently the most important finding of a discovery exercise, and it is a finding that changes the sequence of the plan rather than merely adding to it, because a system you cannot restore is a system you must not experiment on. Whether the backups can really be restored is asked as a database question and answered as a legacy question.

Interviews with people are the other half, and the trick is to ask everybody the same small set of questions so that the disagreements become data. Where several people give the same answer you have a fact. Where one name appears in every answer you have found the load-bearing human. Where the operators and the engineers describe different systems, the operators are usually right, because they are describing the one that runs. The full discovery protocol, including the two-week deliverable, is worked through in you inherit a system nobody understands.

Symptom, cause, first move

Interviewers frequently present a symptom and expect you to reason to a cause before proposing anything, because jumping from symptom to solution is the observable form of pattern-matching without diagnosis. The mapping is worth holding because the first moves are not interchangeable.

SymptomUsual underlying causeThe first move
Every release needs a weekend and three peopleManual deployment steps and no reversible pathAutomate the release before changing any code
Nobody will touch one moduleImplicit coupling nobody can boundTrace it in production, then characterise its behaviour
Bugs reappear months after being fixedNo regression tests, so fixes are not pinnedAdd a test at the moment of each fix, starting now
Two reports disagree about the same numberTwo write paths into one shared storeFind every writer before proposing a single owner
The team estimates everything at three weeksChange cost is dominated by fear, not workMeasure lead time, then remove the largest single blocker
One person answers every questionKnowledge concentrationPair on the next three changes and write down what emerges
Data quality complaints from downstreamA nightly job silently repairing the daytime pathRead the batch layer before touching the online path

The pattern across the right-hand column is that the first move is almost never the structural change. It is the thing that makes the structural change knowable or reversible. Interviewers grade that ordering explicitly, and a candidate who proposes extracting a service as the first move in a system with no tests and a manual release has proposed the fourth step first.

Characterisation tests and getting a seam into untested code

This is the technical core of the subject and the part most candidates have the least practice with, because it is a skill you only acquire in codebases you did not write. Michael Feathers' formulation in Working Effectively with Legacy Code is the standard one: legacy code is code without tests, and the central problem is that to add tests you must change the code, and to change the code safely you need tests. Breaking that circularity is the technique.

A characterisation test is a test that asserts what the code currently does, including where that is wrong. You call the code, capture the output, and write the assertion from the captured value rather than from the specification. It documents behaviour rather than requirement, and its purpose is not to prove correctness but to detect change. If the code rounds a fee down where the policy says round up, the characterisation test asserts round down, with a comment saying so, because your immediate goal is to be able to refactor without altering behaviour, and altering behaviour is precisely what the test must catch. Fixing the rounding is a separate, later, deliberate change with its own approval, and the test you wrote is what proves the fix changed only the thing intended.

The awkward part is getting the code into a test at all, which is where seams come in. A seam is a place where you can change the behaviour of a program without editing the code at that place. Untested legacy code typically has none, because dependencies are constructed inline: the method that calculates a premium also opens a database connection, reads the clock, calls a payment gateway and writes a log file, so calling it from a test means having all four.

The narrowest useful technique is to introduce a seam with the smallest possible edit, verified by eye rather than by test, since you have no test yet. Extracting a dependency to a parameter with a default is often enough:

// Before: no seam. The clock and the rate source are unreachable from a test.
public BigDecimal premium(Policy policy) {
    LocalDate today = LocalDate.now();
    Rate rate = RateTable.lookup(policy.band(), today);
    return compute(policy, rate, today);
}

// After: two seams, and the original signature still behaves identically,
// so no caller changes and the edit is reviewable line by line.
public BigDecimal premium(Policy policy) {
    return premium(policy, Clock.systemDefaultZone(), RateTable::lookup);
}

BigDecimal premium(Policy policy, Clock clock, RateSource rates) {
    LocalDate today = LocalDate.now(clock);           // clock is now injectable
    Rate rate = rates.lookup(policy.band(), today);   // rate source is now fake-able
    return compute(policy, rate, today);
}

Two properties make this safe enough to do without a test. The public signature is unchanged, so no caller is affected and the change cannot ripple. And the delegation is mechanical, so a reviewer can verify equivalence by reading rather than by executing. That combination, a change small enough to verify by inspection which enables a change too large to verify by inspection, is the whole of the technique, and being able to articulate it is what separates a candidate who has read about characterisation tests from one who has written them.

Once a seam exists, the highest-value characterisation is usually not unit-level. Capture real production inputs, ideally a large and varied sample rather than a curated one, run them through the code, record the outputs, and store the pairs as a golden corpus. This finds the behaviour nobody would have thought to write a test for, which is exactly the behaviour at risk. Where the code has no return value and mutates a database instead, the same approach works against a snapshot: run against a copy, diff the resulting state, and assert on the diff.

Where you genuinely cannot get the code under test, the fallback is to characterise from outside: record requests and responses at the process boundary and replay them against the new implementation. This is weaker, because coverage is only as good as your traffic sample and rare paths will be absent, but it is available when nothing else is and it is how most language-level migrations of very old systems are actually verified.

Three cautions belong in a strong answer. A characterisation suite is not a specification and must not be treated as one when the time comes to intentionally change behaviour, or you will find yourself preserving bugs forever out of superstition. Coverage of a golden corpus is coverage of observed traffic, not of the input space, so the quarterly and annual paths need deliberate hunting. And personal data in captured production traffic is a real obligation, so the capture pipeline needs masking designed in from the start rather than added after somebody notices.

The strangler fig, and where it becomes two systems forever

The strangler fig pattern, named by Martin Fowler after the plant that grows around a host tree and eventually stands alone once the host has rotted away, is the default for large modernisations and is also the most frequently misapplied idea in the field. Stated properly it is not "replace it bit by bit". It is: place an interception point in front of the existing system, route a slice of behaviour to a new implementation while everything else continues to the old one, verify the slice, cut it over, remove the old code for that slice, and repeat until the old system carries nothing and can be switched off.

Every clause of that carries weight. Interception has to be possible, which is the first genuine precondition and the one that most often fails. You need a place where requests can be redirected per slice, whether that is an HTTP proxy, an API gateway, a message router, a facade class inside the monolith, or a database view. If requests reach the legacy system through a fat client talking a proprietary protocol to a mainframe transaction manager, and there is no point at which you can intervene, the pattern does not apply until you have built one, and building one is itself a project.

Slices have to be separable, which is the second precondition and the one that fails second most often. A slice is only cleanly extractable if its data is separable. If the candidate boundary reads and writes a table that eleven other modules also read and write, extracting the code moves the problem rather than solving it, and you are choosing between a distributed transaction, a shared database with two owners, or a synchronisation mechanism with a consistency window. That is the substance of one shared database or two copies that disagree, and it is why data, not code, determines the order in which slices can be taken.

The routing layer has to be reversible per slice, not per release. The value of the pattern is that a slice which behaves badly can be sent back to the legacy path in seconds without a deployment, so routing must be configuration that operations can change under pressure, at a granularity that matches what can fail: a customer segment, a transaction type, a percentage of traffic, a single tenant.

And the old code has to be removed. This is the clause that programmes drop, and dropping it is how the pattern degrades into its failure mode. A slice that is cut over but left in place in the legacy system means two implementations of one behaviour, both of which must now receive every regulatory change, every bug fix and every new requirement. The maintenance cost of the estate has gone up, not down. Repeat that across a programme and you arrive at the situation the pattern was supposed to prevent: two systems forever, a routing layer nobody dares simplify, and a team whose velocity is worse than before it started. When an interviewer asks how you know a strangler programme is succeeding, the answer they want is a count of decommissioned components, not a count of delivered ones.

Where the pattern genuinely does not fit

It fits badly when the system is small, because the routing and duplication overhead is a large fraction of the total work and a two-month rewrite would have been finished before the interception layer was built. It fits badly when the slices cannot be made independent, typically because the data model is a single deeply-joined schema that every path touches, in which case the honest answer is that the first phase of the programme is a data separation exercise and the strangling cannot start until it is done. It fits badly when the substrate is being switched off on a fixed date, because a pattern optimised for indefinite incremental progress is a poor fit for a deadline that does not move. And it fits badly when the organisation cannot sustain multi-year attention, which is a real constraint rather than an excuse: a strangler programme that loses its sponsor at month fourteen leaves the estate in a strictly worse state than never having started, because the halfway point is the point of maximum complexity.

That last consideration is a judgement call an architect is paid to make, and saying it out loud in an interview is unusual enough to be a strong differentiator. The incremental path is safer per step and riskier as a programme, because it requires the organisation to keep caring for years. The big-bang path is riskier per step and, occasionally, more likely to finish.

Rewrite or replace incrementally, argued honestly

Candidates arrive at this question with a rehearsed position, and the rehearsed position is nearly always "never rewrite", learned from a well-known essay. It is a reasonable default and it is not an argument, and an interviewer who asks the question is usually looking for the conditions rather than the conclusion.

The honest case for incremental replacement is that it delivers value before it finishes, so it survives a change of priorities, a change of sponsor and a budget cut. It keeps the team's knowledge of the domain fresh, because they are working in the live behaviour rather than reconstructing it. It bounds the blast radius of every mistake to one slice. It lets the business keep changing the system throughout, which matters because the business will insist on this whether or not the plan allows for it. And it produces a continuous stream of evidence about whether the assumptions were right, which is exactly what you lack at the start.

The honest case for a rewrite is narrower but it is real. When the behaviour is externally specified rather than accumulated, as with a protocol implementation, a standards-based interface or a regulatory calculation with a published definition, the great risk of a rewrite disappears, because the specification exists and does not have to be excavated. When the substrate is being withdrawn on a date, incrementalism may not have anywhere to be incremental to. When the system is genuinely small, the overhead of an incremental scaffold can exceed the work. When the new system must satisfy a requirement the old architecture structurally cannot meet, such as multi-region operation, per-tenant isolation or a data residency obligation, incremental change converges on a rewrite anyway and pretending otherwise just adds a routing layer to the bill. And when the old system's data model is the defect, an incremental path that preserves it delivers a modern deployment of the same problem.

What earns credit is stating the preconditions you would insist on before agreeing to a rewrite, because those are what make the difference between a rewrite and a disaster. There must be a freeze or a strictly-controlled change budget on the old system, agreed with the business in writing, because a rewrite racing a moving target loses. There must be a behavioural corpus, captured from production, that both systems can be run against. There must be a parallel-running period rather than a switch. There must be a rollback that works after cutover, not only before it. And there must be an explicit scope rule that the first release is functionally equivalent, with the improvements sequenced afterwards. If a sponsor will not agree to those, the rewrite is not viable and the honest response is to say so rather than to accept the work and hope.

The framing that most reliably moves the conversation forward is to ask what changes when the programme completes. If the answer is a list of engineering properties, the programme is unfundable and should be re-scoped around something the business wants. If the answer is a specific capability the business cannot have today, then the question is which strategy delivers that capability soonest, and the answer is frequently a much smaller piece of work than either a rewrite or a full strangling. Half of good modernisation advice is noticing that the business does not need the whole thing replaced, it needs one thing the system cannot do, and it will settle for that. The pressure to do the bigger thing anyway is the substance of the team wants to rewrite a service that works.

Data migration and the cutover

Code can be run twice. Data can only be somewhere. That asymmetry is why data migration is the part of a modernisation programme that actually fails, and why interviewers who want to find the ceiling of a candidate's experience go here.

The governing requirement is easy to state and hard to satisfy: at every moment, including during the cutover and including during a rollback, a customer's true state must be knowable. Not eventually knowable, not reconstructable from logs by an engineer at three in the morning, but knowable by the systems and the people who have to answer questions about it. Everything else in migration design follows from holding that requirement fixed.

stateDiagram-v2
    accDescr: Migration states from backfilled through dual write, shadow read, new primary and old read-only to decommissioned, with rollback edges leading from shadow read and from new primary back to dual write, and a note that rollback ends at old read-only.
    [*] --> Backfilled
    Backfilled --> DualWrite
    DualWrite --> ShadowRead
    ShadowRead --> NewPrimary
    NewPrimary --> OldReadOnly
    OldReadOnly --> Decommissioned
    ShadowRead --> DualWrite
    NewPrimary --> DualWrite
    note right of OldReadOnly
        Rollback ends here
    end note

The interesting property is where the return edges stop. From shadow reads and from new-primary you can still fall back to the old system as the source of truth, because it has been receiving every write. Once the old system goes read-only you have chosen, and from that point a problem is fixed forward rather than reversed, which is why the transition into read-only is the decision that needs a named owner and an explicit criterion rather than a date in a plan.

Backfill, dual write, shadow read, cut, retire

Backfill is the bulk copy of history, and it is the phase where scale bites. A table you cannot lock has to be copied in chunks with a resumable cursor, keyed on something stable so a restart does not duplicate or skip, and throttled so the copy does not degrade the production system it is reading from. Expect the copy to take long enough that data changes underneath it, which is why backfill alone is never sufficient and must be followed by a catch-up pass or preceded by a change-capture stream. The mechanics overlap heavily with schema migration on a large table without downtime, and the failure modes are the same ones.

Dual write is where both systems receive every change, and it is the phase with the subtlest defects. Writing to two stores is not atomic, so you must decide what happens when the second write fails: fail the request, which makes your availability the product of two systems, or accept the request and repair asynchronously, which means accepting a window of divergence and building the repair. The usual correct answer is to make one system authoritative for the response and to propagate to the other through a durable log rather than a direct call, so a failure is a lag rather than a loss, which is the practical content of queue versus log and what exactly-once means. Say explicitly which system is authoritative during this phase, because a dual-write design that has not named an authority has two sources of truth and no tiebreak.

Shadow reads are where you learn whether the new system is right. Every read is served from the old system and also executed against the new one, the results are compared, and only the old result is returned. This is free verification against real traffic, including the traffic patterns nobody would have thought to test, and it is the phase that most programmes truncate because it feels like it is not progress. It is progress. It is the only phase in which you can be wrong at no cost.

The cut is a routing change, and the property to insist on is that it is a routing change rather than a deployment, so it can be made and unmade in seconds by someone who is not an engineer. Ramp it: one internal tenant, then a small percentage, then a segment, then the rest, with a defined observation period at each step and a defined criterion for proceeding rather than a schedule. Choosing when to do it, and what rollback means at each stage, is worked through in choosing the cutover day and what rollback means.

Retirement is the last phase and the one that needs to be on the plan with a date and an owner, for the reason the roadmap makes visible. Retire in stages: stop writes, then stop reads, then stop the process, then archive the data against the retention obligation, then release the infrastructure and cancel the licence. Each stage is reversible except the last two, and leaving the system running "just in case" for a year is how the saving in the business case fails to materialise.

Reconciliation is the deliverable, not the checkbox

Parallel running without reconciliation is theatre. The reconciliation harness is a real piece of software with its own design, and treating it as such is one of the clearest experience signals in this subject.

flowchart TD
    accDescr: The reconciliation harness, one incoming request answered by the legacy path while the new path computes in shadow, the two outputs compared, and a three-way branch where no difference increases a confidence counter, a known difference is recorded as expected variance, and an unknown one alerts and holds the ramp.
    A[Request arrives] --> B[Legacy path computes and responds]
    A --> C[New path computes in shadow]
    B --> D[Compare outputs]
    C --> D
    D --> E{Difference}
    E -->|None| F[Confidence counter increases]
    E -->|Known and accepted| G[Record as expected variance]
    E -->|Unknown| H[Alert and hold the ramp]

The middle branch is the one that makes the harness usable. Without a way to record a difference as expected and suppress it, the comparison output becomes a wall of noise within a day and the team stops reading it, at which point you have the cost of parallel running and none of the benefit.

Several design decisions are worth being able to name. Comparison must be semantic rather than byte-for-byte, because timestamps, generated identifiers, key ordering and floating-point representation will differ for reasons that do not matter, and a harness that flags all of them is a harness nobody uses. Differences must be classified rather than counted, because a hundred instances of one rounding discrepancy is one problem and one instance of a wrong customer identifier is a serious one, and a count treats them identically. Every accepted variance needs a recorded reason and an owner, so the list is auditable at the point of cutover rather than being a folder of dismissals. Side effects must be suppressed on the shadow path, which is harder than it sounds, since a new implementation that sends the email, charges the card or files the regulatory report while merely shadowing has caused an incident from a verification step. And the comparison must be sampled or asynchronous if it would otherwise put the new system's latency on the critical path.

The exit criterion belongs in the plan before the phase starts. Something of the form: all high-value transaction types observed at least once, every unknown difference class either resolved or accepted with a reason, no new unknown class in the last two weeks, and coverage of at least one full month-end. Cutting over because the date arrived, rather than because the criterion was met, is the decision that produces the incident, and being the person who says so is a large part of what senior means here.

The month-end problem, and other calendars

A modernisation plan built around request-response behaviour will be ambushed by the batch layer. Legacy systems in finance, insurance, telecoms and retail are typically organised around cycles: nightly settlement, weekly billing, month-end close, quarter-end reporting, year-end rollover, an annual renewal run. These paths execute rarely, carry disproportionate business significance, are the least documented, and are frequently implemented in the least accessible part of the estate.

Three consequences follow. Your parallel-running window has to span at least one instance of every cycle, which sets a floor on the calendar time of the programme that no amount of resourcing can compress. Your cutover date must avoid the cycles rather than land near them, and "avoid" means by a margin large enough that a rollback also lands clear. And your characterisation corpus is systematically biased toward daily traffic, so the annual paths need to be hunted deliberately rather than sampled.

The batch window itself is a constraint people outside these environments underestimate. If the overnight processing currently occupies six of the eight hours available, a replacement that is thirty per cent slower does not have a performance problem to be tuned later, it has a scheduling impossibility that invalidates the design, and the time to discover that is during design rather than during the first month-end after cutover. Ask what the window is, ask what happens when it is missed, and treat the answer as a hard requirement in the way turning "must be scalable" into a testable requirement describes.

Anti-corruption layers

When a new component has to talk to a legacy one, the legacy model will try to colonise the new code. The mechanism is ordinary and hard to resist: the legacy system returns a record with forty fields, six of which are meaningful, three of which are overloaded with two meanings depending on a fourth, and one of which is a status code with eleven values and no documentation. The path of least resistance is to pass that record inward, and within a quarter the new service's domain model is a slightly tidier copy of the old one, which means the modernisation has produced a new deployment of the old design.

An anti-corruption layer is a deliberate translation boundary that prevents this. Nothing legacy-shaped passes it. Inside, the new service works with its own model, expressed in the language the business uses now. Outside, an adapter speaks the legacy protocol and maps between the two, and every ambiguity in the old model is resolved at the boundary, explicitly, in code somebody can read.

flowchart LR
    accDescr: The anti-corruption layer as a round trip, the new domain model passing out through a translator to the legacy client and the legacy system, the legacy-shaped response returning through the same translator, and emerging as a domain object or an explicit error back into the new model.
    A[New domain model] --> B[Translator]
    B --> C[Legacy client]
    C --> D[Legacy system]
    D --> E[Legacy shaped response]
    E --> B
    B --> F[Domain object or explicit error]
    F --> A

The node that carries the argument is the last one: the translator either produces a valid domain object or an explicit error, and never a partially-populated object with nulls standing in for values it could not map, because a null that means "the legacy system did not tell us" is exactly how the old ambiguity re-enters the new system.

The translation is where the accumulated knowledge lives, and writing it down there is one of the durable benefits of a modernisation programme even if nothing else survives:

# Every branch here is a piece of institutional knowledge that existed
# only in one engineer's head until this function was written.
def to_account_status(legacy_code: str, opened_on: date) -> AccountStatus:
    if legacy_code == "A":
        return AccountStatus.ACTIVE
    if legacy_code == "S":
        return AccountStatus.SUSPENDED
    if legacy_code == "":
        # Blank means ACTIVE for records created before the 2011 migration,
        # and is an unmapped defect for anything after it. Confirmed against
        # the ops team and by counting rows either side of the date.
        if opened_on < date(2011, 4, 1):
            return AccountStatus.ACTIVE
        raise UnmappedLegacyValue("blank status on a post-2011 account")
    raise UnmappedLegacyValue(f"unknown status code {legacy_code!r}")

Two things about that example matter more than the code. Raising on an unmapped value rather than defaulting is the design decision, because a default silently converts unknown data into confident wrong answers, and in a migration the unknown values are the ones you most need to see. And the comment recording where the knowledge came from is the artefact, since the next engineer's question will be "how do we know that", and an answer of "we counted" is worth more than the mapping itself.

The cost, which you should name before the interviewer does, is that the layer is work that adds no feature and must be maintained for as long as the legacy system exists. That is acceptable when the boundary is long-lived or the legacy model is genuinely toxic, and it is over-engineering when the legacy system is being retired in four months and the integration is one endpoint. Judgement about which situation you are in is the point.

Deciding what not to migrate

The largest single lever on the cost of a modernisation programme is scope, and the largest single lever on scope is the decision not to bring things across. Estates accumulate: features built for a customer who left, reports superseded by a data warehouse, an admin screen for a process that is now automated, a currency the business no longer trades, a channel that was decommissioned everywhere except here. Migrating all of it is the default and it is a choice nobody consciously made.

DispositionTest for itWhat you still owe
Migrate as isUsed, load-bearing, and its behaviour is understoodCharacterisation coverage before you touch it
Migrate simplifiedUsed, but the complexity serves cases that no longer existEvidence that those cases are truly gone
Replace with a manual processVery low volume, and a human can do itAn agreed owner and a documented procedure
Retire outrightTelemetry shows no use across a full business cycleA retention decision for its data and a note to whoever asks
Freeze in placeStill needed occasionally and not worth rebuildingA read-only archive or the old system kept for that path alone

The evidence bar for the last two rows is the part interviewers probe. "Nobody uses it" is a claim, and the way to substantiate it is telemetry over a period long enough to include the quarterly and annual paths, plus a check of the batch layer and the reporting layer, plus asking the support team, who will know about the one customer. A full business cycle is the minimum observation period and it is why instrumenting the legacy system early is worth doing even though it feels like investing in something you plan to delete. It is not investment in the old system, it is the evidence base for the new one's scope.

Freezing in place deserves more attention than it usually gets, because it converts an expensive migration into a cheap one. If a rarely-used but occasionally-essential capability is the reason a decommissioning is blocked, keeping a minimal read-only instance of the old system for that path alone, with an explicit annual review, is frequently a better answer than rebuilding the capability. It is also an answer that requires honesty about the ongoing cost, because a frozen system is still a system that needs patching, and a freeze without a review date becomes a permanent liability nobody remembers agreeing to.

Dead code, and the code that only looks dead

The difference between dead code and rarely-executed code is one calendar year, and the cost of getting it wrong is asymmetric: leaving dead code costs maintenance, and deleting live code costs an incident in a path that, by construction, nobody was watching.

The safe procedure is worth being able to state in order. Instrument the entry point with a distinctive, cheap, always-on counter, and deploy it. Wait a full business cycle, at minimum, including whatever the annual events are for this business. Search for callers outside the codebase, which means the database for triggers and views, the scheduler for jobs, other repositories for direct calls, the API gateway for routes, and the partner documentation for anything published externally. Announce the intention where affected people will see it and allow time for someone to object, because the finance analyst who runs the year-end extract does not read your deprecation channel. Then remove it behind a flag or a routing rule that can restore it quickly, and only after another cycle delete the code. Interviewers grade the presence of the observation period and the external-caller search, because those are the two steps that distinguish a procedure from a hope. The generalisable form of the reasoning appears in what a deleted-at column costs you a year in, where the same tension between removing and retaining plays out in a schema.

There is a related category that is more dangerous, which is code that runs and has no effect: a listener whose handler has been an empty method since 2019, a validation whose result is discarded, a retry loop around a call that has been pointed at a decommissioned host for two years and fails silently. These are not dead, they are inert, and the reason they matter is that somebody upstream may believe they are working. Finding one during discovery is usually the most useful thing you will find that week.

Vendor pressure, end of life, and the clock you do not own

A significant proportion of real modernisation work is not chosen, it is triggered: a vendor announces end of support, a platform is sunset, a licensing model changes, a supplier is acquired, a runtime stops receiving security patches, or a compliance regime stops accepting the version you run. The shape of the problem changes when the clock is not yours, and interviewers use this as a scenario precisely because it removes the option of deferring.

The first move is to establish what the date actually means, because vendors publish several and they are not equivalent. End of sale means you cannot buy more. End of standard support means no new fixes but often a paid extension. End of extended support means no security patches, which is usually the date your risk function cares about. End of life for the underlying hardware or hosting is a different date again, and is the one that cannot be negotiated with money. Candidates who treat a single announced date as the deadline have skipped the cheapest option available, which is buying time.

Buying time is a legitimate strategy and should be evaluated rather than dismissed. Extended support contracts, a version upgrade within the same product to reset the clock, moving the workload to a supported hosting arrangement, or accepting a documented risk for a bounded period with compensating controls, are all real answers. Each converts a hard deadline into a cost, and comparing that cost against the cost of a rushed migration is exactly the analysis an architect is there to do. A migration executed badly because the date was tight is more expensive than the support contract that would have prevented it, and saying that clearly in front of a sponsor who wants to look decisive is the harder half of the job.

The second move is to establish the blast radius, because "the vendor is going away" is rarely uniform across an estate. Which of your capabilities depend on the product, which of those have alternatives, which are load-bearing for revenue, and which could be retired instead of replaced. It is common to find that a platform serving nine functions could be replaced for the two that matter, with three retired, three moved to something you already own, and one frozen. That analysis turns an unaffordable programme into an affordable one and it is the work interviewers are hoping to see, as a vendor in your critical path announces end of life sets out in detail.

The third move is to notice what the deadline does to your strategy choice. Incremental replacement assumes you can take as long as the work requires. A fixed external date inverts that: scope becomes the variable and the date does not move, which argues for a smaller target, more aggressive retirement, and a willingness to accept a temporary compromise such as running the new system with a legacy-shaped data model until after the deadline has passed. Being able to say "the deadline changes which strategy is correct, and here is how" is worth more than any individual technique.

Mainframe, COBOL and batch, without pretending

Many candidates will be interviewed about environments they have not worked in, and the correct behaviour is neither to bluff nor to disclaim. Interviewers in enterprise and consulting settings frequently ask about mainframe modernisation knowing the candidate has not touched one, because what they are grading is whether you can reason about an unfamiliar constraint set without either inventing expertise or freezing.

What is worth knowing, stated at the level you can genuinely defend: a great deal of transaction processing in banking, insurance, government and airlines still runs on mainframes, typically COBOL programs driven by a transaction manager for online work and by a job scheduler for batch, over hierarchical or relational data. These systems are usually extremely reliable and extremely fast at what they do, which is the fact outsiders get wrong most often. They are not modernisation candidates because they perform badly. They are candidates because the people who understand them are retiring, because change is slow and expensive, because the licensing and capacity model is costly, and because the batch-oriented shape makes real-time requirements awkward to satisfy.

The properties that change your design thinking are worth naming. Processing is organised around a batch window, so throughput within a fixed period matters more than per-request latency. Integration is often file-based, and files have record layouts, fixed-width fields and code pages, so character encoding and packed decimal representation are genuine sources of migration defects rather than trivia. Numeric behaviour is decimal, not binary floating point, and a replacement that computes interest in a binary float will produce differences that are individually tiny and collectively unacceptable. Availability expectations are usually higher than a distributed replacement can casually match, which makes the availability target itself something to negotiate rather than assume, along the lines of choosing an availability target and what it costs.

The strategies discussed elsewhere in this page all appear here in specific forms. Encapsulation means putting a service interface in front of existing transactions so new consumers get a modern contract without the core changing, which is usually the correct first move and buys years. Automated translation of source into another language is available and produces code that works and is not idiomatic, so it solves the substrate problem and not the comprehension problem, which is a genuine trade rather than a trap. Re-hosting moves the workload to different infrastructure while keeping the programs, which addresses cost and hardware risk without touching behaviour. Rewriting the business logic against a fresh model is the most expensive and the only one that addresses comprehension. Naming all four and saying which problem each solves is a much stronger answer than advocating one.

How to handle the knowledge gap is straightforward and interviewers respect it: say what you know, say what you would need to establish, and say who you would ask. "I have not run a mainframe programme, so before proposing anything I would want the batch window, the transaction volumes and their profile, the file interfaces and their record layouts, the licensing model, and an honest count of the people who can still change COBOL here, because that last number sets the pace of everything." That answer demonstrates the reasoning without the pretence, and it is markedly better received than a confident recitation of terminology.

Keeping the lights on while modernising

Every modernisation runs alongside the ordinary business of operating and changing the existing system, and the interaction between the two is where programmes are lost. This is the part that is organisational rather than technical, and senior interviews spend real time on it.

The first tension is capacity. The people who understand the legacy system are the same people needed to build the replacement and the same people who get paged when it breaks. Splitting them across all three guarantees all three go slowly, and the failure mode is a modernisation that receives whatever attention is left after operations, which over a year is close to none. The structures that work all involve protecting the modernisation capacity explicitly: a separate team with a clear interface to the legacy team, a rotation that moves people between the two on a fixed cadence rather than by interruption, or an agreement that on-call is staffed from a defined subset. The structure matters less than the fact that somebody decided it deliberately, which is the argument in how much capacity goes to roadmap versus keeping the lights on.

The second tension is change during the programme. The business will require changes to the legacy system while you replace it, and a plan that assumes otherwise is wrong on day one. The workable position is a change budget rather than a freeze: changes continue, they are prioritised jointly, and every change to a slice that is currently being replaced has to be made twice, which is a cost the requester should see. Making the double-implementation cost visible at the point of request is what stops the change volume quietly destroying the programme, and it converts an engineering complaint into a business decision, which is where it belongs.

The third tension is that the legacy system continues to accrue debt. It is tempting to declare it frozen and stop investing, and that is correct for structural work and wrong for operability. Keep patching, keep it observable, keep the backups verified, keep the runbooks current. A legacy system that fails badly in month nine of a three-year programme will consume the programme, and the cheapest insurance is that the thing you are replacing remains boring while you replace it.

The fourth is that the modernisation must not become invisible. Programmes that deliver nothing for eighteen months lose sponsors regardless of how well they are going, because a sponsor with no evidence has nothing to defend at a budget meeting. This is the strongest practical argument for incremental delivery and it is an argument about organisational survival rather than about engineering, which is why it belongs in the plan and not just in the retrospective.

Sequencing a multi-year programme so value lands early

A three-year modernisation that delivers its first value in year three is a three-year bet that nothing changes: no reorganisation, no budget cut, no change of sponsor, no acquisition, no downturn. Nobody would write that risk down and accept it if it were stated plainly, and yet it is the implicit shape of most large plans. Sequencing is the discipline of arranging the work so that the programme is valuable at every point at which it could be stopped.

Four ordering principles carry most of the weight. Sequence by risk retired per unit of effort early, then by value delivered, because the first thing a programme needs is evidence that its central assumptions hold. The first slice should therefore be chosen for what it teaches rather than for what it is worth: something real enough that its lessons transfer, small enough to finish in weeks, and connected enough that it exercises the routing, the data path, the deployment and the reconciliation end to end. A first slice that avoids every hard part proves nothing and creates false confidence, which is worse than no confidence.

Sequence to decommission early and often. Every retired component is a permanent reduction in the estate, an unambiguous unit of progress a sponsor can understand, and a real saving. Two small components retired in the first six months does more for a programme's political survival than one large component half-built.

Sequence around the data. Slices are constrained by which data can be separated, so the dependency graph of the programme is drawn on the schema rather than on the code. Doing this analysis before writing the plan is what prevents the situation where slice four turns out to be impossible until slices seven and nine are done.

Sequence so that the business gets something it asked for within the first two quarters. Not a technical milestone, a capability. This is what converts the programme from an engineering cost centre into something with an advocate outside the engineering organisation, and advocates outside engineering are what programmes survive on when budgets tighten.

Two anti-patterns are worth naming because interviewers listen for them. The first is the platform-first sequence, where the first year builds infrastructure, tooling and frameworks in anticipation of migrations that have not started, so the assumptions are never tested against a real workload and the platform is wrong in ways nobody discovers until it is expensive. Build the platform out of the first two or three migrations rather than before them. The second is the easy-slices-first sequence, where the peripheral components go across quickly, the burndown looks excellent for a year, and then the programme arrives at the core with its remaining budget and every hard problem still ahead of it. Take at least one genuinely hard thing early, so the estimate for the rest is grounded in something.

Progress reporting deserves a sentence, because it drives behaviour. A programme measured on components migrated will migrate components. A programme measured on components decommissioned, legacy code lines removed, legacy infrastructure spend eliminated and manual processes retired will do the thing you actually want. Choose the second set, and accept that it makes early progress look slower, because the alternative is a metric that rewards leaving the old system running.

The business case, and defending "do nothing yet"

At architect level you will be asked to justify a modernisation to people who control money, and the interviewer is grading whether you can hold an engineering position in a commercial conversation without either collapsing into technical detail or over-promising.

Start from what the business cannot currently do, not from what the system is. Cost, time to market, risk exposure and constraint are the four currencies that translate. Cost means run cost, licence cost, and the cost of the effort currently consumed by maintenance that would be released. Time to market means the specific change that takes six months today and would take two weeks, ideally one the sponsor has personally been frustrated by. Risk means an exposure with a probability and a consequence, expressed in the language the risk function already uses, whether that is regulatory, security, continuity or key-person. Constraint means the product, market or deal that cannot be pursued at all.

Be honest about the shape of the return, because everyone in the room has seen a modernisation business case before. The costs are near-term and reasonably estimable. The benefits are later and less certain. The programme will take longer than the estimate. There is a period in the middle where the organisation is running two systems and its costs are higher than either endpoint. Presenting a case that hides the trough is the fastest way to lose credibility with a finance function that will find it anyway, and presenting it openly, with the trough sized and the exit criteria stated, is unusual enough to be persuasive.

Give the option set rather than a single proposal. A sponsor presented with one option is being asked to approve, and a sponsor presented with three sized options with their consequences is being asked to decide, which is what they are for. Include the do-nothing option honestly costed, including the cost of continuing to run the current system and the cost of the risk being carried, since the comparison is meaningless without it.

And be prepared for the answer to be that doing nothing is correct. It genuinely is correct more often than modernisation enthusiasm suggests: when the system is stable, the substrate is supported, the change rate is low, no business capability is blocked, and the capital would return more elsewhere. Defending that position is a distinct skill and it has a specific structure. State the conditions under which you would revisit, as concrete triggers rather than as sentiment: the support date, a change rate above some threshold, the departure of the second-to-last person who understands it, a specific product decision that would require it. Put a review date in the calendar with an owner. Do the cheap risk mitigations now, since documenting the system, adding observability, verifying restores and reducing the key-person dependency cost a fraction of a modernisation and remove most of the near-term danger. And write it down as a decision with its reasoning, because in two years somebody will ask why nothing was done, and the difference between a decision and negligence is the record, which is the argument in what belongs in an ADR and when to write one.

The human side

Every legacy system has at least one person who understands it, and how you treat them determines whether the programme works. This is not a soft topic appended to a technical one. It is a first-order risk and interviewers at senior level ask about it directly.

The situation from their side is worth thinking through before you propose anything. They have spent years accumulating knowledge that is valuable only while the system exists. They are frequently the reason the organisation has not had a serious incident. They are also, often, tired of being the only one, and have watched two previous modernisation attempts start and stop. A programme that arrives describing their system as legacy, their code as debt and their expertise as an obstacle will get compliance and not cooperation, and the difference is entirely invisible until you discover that the thing you needed to know was never mentioned.

What works is making them central rather than peripheral. The knowledge extraction that a modernisation requires is exactly the work they are best placed to lead, and framing it as authorship rather than as a handover changes how it goes. Pair them with the people building the replacement rather than interviewing them; the questions that arise while building are better than the questions anyone thinks to ask in a meeting. Be explicit and early about what their role is after the programme, because the unspoken version of that question is the one determining their behaviour. And treat the reduction of the key-person dependency as a benefit to them, since being unable to take a holiday without a rota change is not a privilege.

There is also a straightforward continuity argument to make to management, which is that a single point of human failure in a business-critical system is a risk that would be unacceptable if it were a server. Pairing, documentation written by the expert and verified by someone else attempting the task from it, a rotation of the on-call for that system, and a deliberate second person for each critical area are the mitigations, and they are cheap relative to the exposure. Losing that person mid-programme is a scenario worth having thought about, and it connects to the reasoning in your best engineer tells you they are leaving.

The other human dimension is the team doing the modernisation. This work is unglamorous, slow, and rewarded less than feature delivery in most performance systems. It involves reading other people's code, writing tests for behaviour you disagree with, and building things whose success is measured by something being switched off. Sustaining a team through three years of that requires the progress to be visible, the decommissions to be celebrated, and the promotion cases to be written so that "removed nine years of accumulated risk from the payments path" reads as the achievement it is. An architect who has not thought about that is planning a programme whose best engineers leave in year two.

What interviewers ask

Interviewers in this area are grading a small set of observable behaviours, and they are nearly the same behaviours whether the question is about characterisation tests or about a business case. Knowing what they are lets you supply the signal deliberately rather than hoping it emerges.

The first is whether you establish evidence before proposing action. Every legacy scenario is under-specified on purpose, and the strongest opening move is almost always a question about what is known: what does this system do for the business, what is the change rate, what breaks, what is the deploy path, who depends on it, and is there a date. A candidate who begins designing the target state has told the interviewer they will do the same thing on the job.

The second is whether you sequence stabilisation before structure. Making a system observable, deployable and restorable is prerequisite to changing it, and a plan that begins with extraction is a plan that will find out about its mistakes from customers. This is graded on ordering rather than on content, and it is the most common single point of failure in otherwise strong answers.

The third is whether you preserve the ability to go back. At every stage of a modernisation there should be an answer to what happens if this is wrong, and the answer should get less pleasant as you approach cutover but should never be absent until the point you deliberately chose. Candidates who can name where their rollback ends, and who owns the decision to cross that line, are demonstrating something that cannot be faked.

The fourth is whether you treat the old system's behaviour as authoritative. The instinct to correct while migrating is strong and the discipline to characterise first, migrate faithfully and correct afterwards is a learned one. An interviewer will often plant an obviously wrong behaviour in the scenario to see whether you fix it in flight.

The fifth is whether you name the decommissioning. Any plan that lacks an explicit retirement phase with an owner and a date is a plan to run two systems, and interviewers who have lived through a stalled programme listen for this specifically.

The sixth is whether you can say who decides. Cutover timing, accepted variance, scope reduction, risk acceptance and the choice to defer are all business decisions informed by engineering rather than the reverse, and an answer that quietly assumes the engineer decides has misread the room.

The seventh is precision about the unknown. Saying which things you do not know, what it would cost to find out, and what you would do differently depending on the answer, is the single most reliable senior signal in this subject, because the defining condition of legacy work is that the information is missing and pretending otherwise is the characteristic failure.

Question archetypeThe signal a strong answer supplies
You inherit a system nobody understandsEvidence from the running system before any target state
Should we rewrite thisThe conditions under which each answer is right, then a recommendation
Design the migrationWhere rollback ends, and who owns crossing that line
The reconciliation disagreesClassify before counting, and hold the ramp rather than the date
Justify this to the boardBusiness currency, an honest trough, and a costed do-nothing option
The vendor is switching it offWhich date, what it can be bought back for, and what can be retired instead
One person understands itContinuity treated as an architectural risk with named mitigations

How the same question is scored at different levels

The questions barely change between a senior engineer and a principal architect interview; what changes is the scope of answer that satisfies. At senior engineer level, a good answer is technically correct within the system: the seam, the characterisation corpus, the dual-write path, the reconciliation, the ramp. At staff and principal level it has to cross a boundary into sequencing and consequence: which slice first and why, what the business is told, what happens to the team that maintains the old one, how the programme survives a budget cut. At architect and consulting level the answer is expected to change the shape of the problem, most often by reducing it, by finding that half the estate can be retired, that the real requirement is one capability rather than a replacement, or that the correct decision this year is to buy support and revisit.

What candidates over-prepare, and what they skip

Candidates over-prepare the pattern vocabulary. Being able to define the strangler fig, the anti-corruption layer, the branch by abstraction and the parallel run is table stakes and takes ninety seconds of interview time. Nobody is hired for the definitions.

Candidates skip the batch layer almost universally, skip data migration mechanics, skip the decommissioning phase, and skip who decides. They also skip cost. An architect who cannot put an approximate shape on what a programme costs and where the money goes is not going to be trusted with one, and the shape does not have to be precise, it has to be reasoned: this many people, this long, this much duplicate running cost during the overlap, this much licence saving at the end, this much contingency because discovery is incomplete. Reasoning visibly from stated assumptions is what is being graded, not the number. That is the same habit as where cloud spend goes and how you reduce it applied to a programme rather than to an estate.

Questions

What follows is a working set of questions phrased the way interviewers put them, grouped by the part of the problem they sit in, each with the answer that earns credit and the signal it supplies.

Defining the problem

What makes a system legacy?

The inability to change it safely, which means the absence of a feedback loop that tells you whether a change was correct before customers do. Say that first, then say what age has to do with it, which is nothing directly and something indirectly: old systems have had more time to lose their tests, their authors and their documentation, so age correlates with legacy without causing it.

The credit comes from decomposing the inability into its causes, because the causes determine the plan and they are not interchangeable. No tests, no deployment path, no comprehension, no supported substrate, no person who understands it. A real system has three of them, and naming which three is the first step of any assessment. An interviewer will often follow up by asking which is worst, and the defensible answer is the deployment path, because it constrains the rate at which you can fix any of the others.

Is technical debt the same thing as legacy?

No, and the distinction is worth being precise about because it changes who you talk to. Technical debt is a deliberate or accreted shortcut whose interest is paid in slower change, and the metaphor implies a decision, a lender and the option to repay. Legacy is a state in which the change mechanism itself has failed, which is a different condition and is often not the result of any decision at all, merely of time and staff turnover.

The practical consequence is that debt is negotiated inside an engineering conversation about capacity allocation, whereas legacy is a business risk conversation about continuity, cost and constraint. Bringing a debt argument to a board is what makes modernisation unfundable; bringing a continuity and capability argument is what funds it. The allocation half of that is worked through in splitting engineers between features and debt.

How do you decide whether a system needs modernising at all?

Against four triggers rather than against an opinion. Is a business capability blocked by it. Is there a clock on the substrate, from a vendor, a regulator or a supplier. Is the cost of change or of running it rising faster than the value it delivers. Is there an unacceptable continuity risk, whether that is a person, a dependency or a restore that has never been tested. If none of those is present, the correct answer is to leave it alone, mitigate the cheap risks and set a review date.

What earns credit is volunteering the fourth quadrant, which is that an ugly system nobody needs to change is not a problem, and an elegant system that blocks a product launch is. Interviewers ask this to find out whether your trigger is aesthetic or economic, and candidates who reach for the stack rather than for the constraint have answered.

Understanding what you have

You have inherited a system nobody understands and two weeks to produce a plan. What do you do?

State the deliverable first, because it is the part candidates get wrong: two weeks buys evidence and a first move, not understanding, so the output is a risk-ordered plan with the unknowns named rather than a target architecture. Then spend the first week on running-system evidence in priority order: money and traffic, the infrastructure bill, six months of incidents and support tickets, the last hundred deploys, database access sources, the scheduler, and one actual restore. Interview widely with a fixed question set so disagreements become data, and include operators and support rather than only engineers, because they describe the system that runs.

Then produce four short artefacts: a one-page context diagram with anything unverified marked as unverified, a risk register of no more than ten entries ordered by expected harm, a ninety-day proposal sequenced as stabilise then make change safe then change structure, and an explicit unknowns list. The two answers that lose the question are arriving with a target architecture and proposing a rewrite, the second being tempting for exactly the reason it is wrong. The full protocol is in you inherit a system nobody understands.

Where do you look for the behaviour that is not in the requirements?

In four places, and naming them specifically is the signal. Production traffic, sampled over long enough to include the rare paths, because the system's actual input distribution is the only honest specification of what it handles. The support ticket history, because every accumulated workaround and tolerated quirk generated a conversation at some point. The batch and reporting layer, which is where the undocumented business rules concentrate and which most discovery exercises never open. And the data itself, because twenty years of rows contain every state the code has ever been able to produce, including the ones the current code cannot, which tells you what the old versions did.

Add the negative result that matters: the requirements document and the code comments are the two lowest-value sources, because both describe intentions. A candidate who ranks the sources rather than listing them has demonstrated they have done this.

How do you tell the difference between essential complexity and accidental complexity in a system you did not write?

By finding out why it is there before deciding it is unnecessary, which usually means archaeology rather than reading. Version control history around the code in question, the ticket referenced in the commit message, the incident that preceded it, and the person who can still remember. A branch that looks arbitrary is frequently a customer contract, a regulatory exception, a workaround for a downstream system's defect, or the fix for an incident that cost somebody their weekend.

The honest position is that you often cannot tell, and the way to act under that uncertainty is to preserve the behaviour, characterise it, and record the question rather than resolving it by assumption. This is a place where interviewers watch for humility with a plan attached: "I would not know, so I would keep it and write down that I do not know why" is a better answer than a confident classification.

Getting the system under test

Explain a characterisation test to someone who writes test-driven code.

It is a test whose assertion comes from the code's observed output rather than from a requirement, and whose purpose is change detection rather than correctness. You run the existing code with real inputs, capture what comes back, and assert that. Where the behaviour is wrong, the test asserts the wrong behaviour, with a comment recording that it is known to be wrong, because the immediate goal is to be able to refactor without changing behaviour and the test's job is to catch exactly the thing you are trying not to do.

The follow-up is usually whether that means you are cementing bugs, and the answer is that you are making them visible and deliberate. A characterised bug is one somebody can decide about; an uncharacterised one is one that gets fixed by accident during a refactor, at which point you have changed observable behaviour for customers with no decision, no communication and no test to tell you.

There is one method you need to change and it cannot be called from a test. Walk me through what you do.

Introduce a seam with the smallest edit you can verify by reading, since you have no test yet. Typically that means extracting the inline dependencies into parameters on an overload, keeping the original signature delegating to the new one with the real implementations, so no caller changes and the diff is mechanically reviewable. Now the code is reachable with a fake clock, a stub rate source or an in-memory repository, and you can write characterisation tests against it.

The nuance that earns credit is naming the risk you just took. That first edit is unverified by definition, so it must be small, delegation-only and reviewed by someone else, and if it cannot be made small then the correct move is to characterise from further out, at the process or HTTP boundary, rather than to make a large unverified change. Also say what you would do with a legacy language that has no dependency injection available: a link-time or configuration seam, a subclass overriding the awkward method for the test, or an interception at the boundary, because the concept of a seam is not tied to a language feature.

How do you build a golden corpus, and what is it blind to?

Capture real production inputs and their outputs at a boundary, store the pairs, and replay them against any changed implementation. Prefer volume and variety over curation, because the value is in the cases nobody would have written. Where the code mutates state rather than returning a value, snapshot the store before and after and assert on the diff.

Then say what it cannot see, which is the part that separates a considered answer. It covers the input distribution you observed, so rare paths, month-end, year-end and error handling are systematically under-represented and need deliberate hunting. It cannot distinguish correct behaviour from incorrect behaviour, only changed from unchanged. It contains personal data, so masking has to be designed into the capture rather than retrofitted. And it goes stale, since a corpus captured a year ago describes a system that has changed since.

The team says the codebase cannot be tested. How do you respond?

By narrowing the claim, because it is nearly always a claim about the current shape rather than about possibility. Ask for one specific change they need to make and cannot verify, and work that single case end to end: find the boundary at which the behaviour is observable, get a test at that boundary even if it is coarse and slow, then make the change. One demonstrated case changes the conversation in a way that an argument does not.

The second half of the answer is that the alternative to tests is not nothing, it is a slower and more expensive feedback loop, and interim options exist: a staging environment fed with production-shaped traffic, feature flags that limit exposure, a canary release to one tenant, and reconciliation against the old behaviour in production. Those are how you make change safe while the test coverage is being built, rather than a substitute for building it.

Strategy and pattern choice

What does the strangler fig pattern require that people forget?

Four things. An interception point where traffic can be redirected per slice, which frequently does not exist and has to be built before the pattern can start. Separable data, because a slice whose tables are shared with eleven other modules cannot be extracted without solving the consistency problem first. Reversible routing at a granularity that matches what can fail, changeable by operations in seconds without a deployment. And the removal of the old implementation once a slice is cut over, which is the clause programmes drop.

Say what dropping the fourth one produces, because that is the answer being looked for: two implementations of the same behaviour, both receiving every future change, an estate more expensive than the one you started with, and a routing layer nobody dares to simplify. The measure of a strangler programme is components decommissioned, not components delivered.

When would you not use it?

When the system is small enough that the scaffolding costs more than the rewrite. When the data cannot be separated, in which case the first phase is a data separation exercise and the strangling cannot begin until it is done. When an external date makes indefinite incrementalism the wrong shape. And when the organisation cannot sustain a multi-year commitment, because the halfway point of a strangler programme is the point of maximum complexity and abandoning it there leaves the estate worse than never having started.

The last one is the differentiator, because it is an organisational judgement rather than a technical one, and it is the reason some architects choose a riskier big-bang path in a company they do not trust to stay interested. Saying that out loud demonstrates you have seen a programme lose its sponsor.

Talk me through encapsulation as a strategy. Is it not just deferring the problem?

It is deferring the internal problem and solving an external one, and whether that is right depends on which problem is costing you. Putting a modern service interface in front of an unchangeable core means new consumers get a clean contract, integration stops requiring knowledge of the legacy protocol, and you gain the interception point the strangler fig will later need. The core is untouched, so none of the comprehension, testing or substrate problems are addressed.

That makes it the right first move surprisingly often, because it is cheap, low-risk, and it buys optionality: with the facade in place, replacing what is behind it becomes a decision you can take later, per capability, without every consumer being involved. It is the wrong move when the driver is a substrate deadline, because the facade does nothing about the thing being switched off, and when it is used to declare victory, since a modernisation programme that has delivered only facades has changed no risk.

The business wants to buy a package instead. How do you evaluate that?

By separating the capability from the process. A package is a good fit where the capability is genuinely undifferentiated and where the business is willing to adopt the product's process rather than configure the product into a copy of the current one, and that second condition is where these decisions fail. The cost of a package is not the licence, it is the integration, the data migration, the process change and the customisation that reappears as an upgrade blocker three years later.

What earns credit is naming the specific things to establish before deciding: which of your current behaviours the product does not support and whether the business will give them up, what the integration surface is and who builds it, what happens at upgrade time to whatever you customise, what the exit looks like, and whether the differentiating part of the process can stay in your own systems with the commodity part in the package. The general framing is in build versus buy for something that is not your differentiator.

Two teams have independently started replacing overlapping parts of the same legacy system. What do you do?

Establish what is actually overlapping before intervening, because two teams building adjacent capabilities is normal and two teams building the same one is a governance failure. Look at the data each intends to own, since duplicate ownership of the same data is the definition of the problem and duplicate code is often not. Then decide ownership explicitly rather than letting it resolve by which team ships first, because the losing implementation left running becomes exactly the two-systems-forever outcome the programme is supposed to avoid.

The senior move is to fix the cause rather than the instance, which is usually that there is no visible target architecture and no forum in which a team announces intent, so both teams behaved reasonably. Two teams building the same capability works through the intervention and the aftermath.

Rewrites

When is a rewrite the right call?

When the behaviour is externally specified rather than accumulated, because then the great risk of a rewrite, which is omitting behaviour nobody can describe, largely disappears. When the substrate is being withdrawn and there is no incremental path onto a supported one. When the system is small enough that the incremental scaffolding costs more than the work. When the requirement is structural, such as multi-region, per-tenant isolation or data residency, and the existing architecture cannot express it, so incremental change converges on a rewrite anyway. And when the data model itself is the defect, since an incremental path that preserves it delivers a modern deployment of the same problem.

Then state the preconditions, because they are what distinguish a rewrite from a disaster: an agreed change budget on the old system, a behavioural corpus captured from production, a parallel-running period, a rollback that works after cutover, and a hard rule that the first release is functionally equivalent with improvements sequenced afterwards. If a sponsor will not agree to those, say the rewrite is not viable, because that refusal is more useful to them than an acceptance you cannot deliver on.

What is the second-system effect and how do you prevent it?

Brooks' observation that the second system an architect designs is the most dangerous, because it collects every idea that was left out of the first. In a modernisation it appears as a replacement that is generalised, configurable and abstracted against requirements that have not arrived, while the specific behaviour of the original is still unfinished, and it is fatal because the legacy system keeps accruing changes and the gap widens faster than the replacement closes it.

Prevention is a scope rule enforced socially rather than a technique: the first release does what the current system does, no more, and every proposed improvement is written on a list that is delivered after cutover. The reason the rule works is not that the improvements are bad, it is that after cutover they are cheap and reversible, whereas before it they sit on the critical path of a programme that cannot afford delay. Interviewers listen for the mechanism, not the definition.

Your rewrite is eighteen months in, the legacy system has moved on, and the gap is growing. What now?

Stop and re-plan, and say that before saying anything else, because the interviewer is testing whether you can call a programme rather than defend one. Then quantify the gap honestly: how much of the legacy change since the start is unimplemented, what the current rate of divergence is, and whether the replacement's completion rate exceeds it. If it does not, the programme cannot finish by continuing, and no amount of additional resourcing changes an arithmetic problem into a staffing one.

The options are worth laying out rather than picking one. Reduce scope drastically to a subset that can be cut over within a short horizon and strangle the remainder, which converts a failing rewrite into a viable incremental programme and is usually the right answer. Freeze the legacy system properly with business agreement, which is only available if the pending changes are genuinely deferrable. Or stop, take the parts that have value such as the tests, the domain model and the extracted knowledge, and return the rest, which is the answer nobody wants and is occasionally correct. What loses the question is asking for more people, because it is the response that shows you have not diagnosed the mechanism.

Data migration and cutover

Design the migration of a large table from an old system to a new one while both are live.

Sequence it as backfill, dual write, shadow read, ramp, cut, retire, and be specific about what each phase protects against. Backfill is a chunked, resumable, throttled copy keyed on something stable, and it must be followed by a catch-up or preceded by change capture, because the data moves underneath it. Dual write means naming which system is authoritative for the response and propagating to the other through a durable log rather than a synchronous call, so a failure is lag rather than loss. Shadow reads compare the new result against the old on live traffic while returning only the old one, which is the only phase in which being wrong is free. The ramp is a routing change at tenant or percentage granularity, changeable in seconds by someone who is not deploying. Retirement is staged: stop writes, stop reads, stop the process, archive against the retention obligation, release the infrastructure.

Then say where rollback ends. Until the old system stops receiving writes you can fall back to it as the source of truth; after that you are fixing forward, and the transition into read-only needs a named owner and a criterion rather than a date. Candidates who volunteer that boundary unprompted are demonstrating the thing this question exists to find.

The cutover is Friday and the reconciliation shows nine hundred mismatches out of four million. What do you do?

Classify before you decide anything, because nine hundred is not a number that tells you anything. Group by difference type and by field, and the distribution will usually collapse into a handful of classes: a rounding convention, a null-versus-empty-string representation, a timezone boundary, a legitimate data-quality defect in the old system that the new one rejects, and, somewhere in there, possibly one class that is a real bug in the new implementation.

Then decide per class rather than in aggregate. Formatting and representation differences with no business consequence are accepted with a recorded reason and an owner. Old-system defects that the new system exposes are a business decision about which behaviour is correct and need somebody outside engineering to make it. An unexplained class is a hold, regardless of how small it is, because an unexplained difference means the model of the system is wrong and the size of the class is not evidence about the size of the underlying problem. Say plainly that you would move the date rather than cut over with an unexplained class, and say who you would need to agree that, because the willingness to hold the ramp is precisely what is being graded.

What does rollback mean the day after a cutover?

It depends on whether writes are still reaching the old system, and answering with that conditional is the answer. While dual write is active, rollback is a routing change and the old system is current, so it is fast and near-free. Once the old system is read-only, rollback means replaying everything written since the cut back into it, which requires that you built that replay path in advance and tested it, and if you did not then rollback is not available and you should not have entered the state.

Add the operational consequences most candidates skip: anything with an external side effect during the window, such as emails sent, payments taken, files transmitted to a partner or reports filed, is not reversed by a routing change and needs a separate remediation plan. Rollback is therefore not one decision but a set of them, and the honest artefact is a written statement per stage of what can and cannot be undone, agreed before the cutover rather than discovered during it, which is what choosing the cutover day and what rollback means is set up around.

How do you migrate a system that cannot be switched off, even for a minute?

By never requiring it to be off, which means every transition is a routing change rather than an outage and every state has both systems able to serve. Practically that is the dual-write and shadow-read sequence, plus a data path that tolerates both systems writing during the overlap, which usually means avoiding shared auto-increment identifiers, agreeing an idempotency key that both sides honour, and deciding conflict resolution in advance rather than discovering it.

The part that earns credit is stating what makes this impossible and how you would negotiate it. Some changes genuinely cannot be made online, typically because a single-writer invariant has to hold across the transition, and the answer then is a very short, rehearsed, tightly-scoped window agreed with the business, not a pretence that it can be avoided. Distinguishing "cannot be off" from "prefers not to be off" is the first question to ask, and the detail is in replacing a system that cannot be switched off.

The migration ran overnight and you cannot tell whether a customer's balance is right. Talk me through it.

Treat it as an incident rather than as a data problem, and start by stopping the harm: prevent further writes on the affected population rather than on everything, since a blanket freeze is usually unnecessary and expensive. Then establish which population is affected and, critically, whether you can bound it, because an unbounded population means every customer's state is in doubt and that is a materially different conversation with a materially different escalation.

Then re-derive rather than repair. If the old system's data is intact and its writes were preserved, the answer is authoritative and the new state can be recomputed rather than patched, which is why the design decision to keep the old system writable through the overlap is worth its cost. Where the source is also damaged, the ladder is the change log, then the backup plus replay, then reconstruction from downstream systems that received the values, then finally asking the customer, which is the point at which the incident becomes a trust problem rather than a technical one. Say explicitly that customer-facing communication starts while the investigation is running rather than after it, because a customer who is told their balance is under review keeps their trust and one who discovers it does not. The generalisable version of the recovery ladder is in a bad delete nobody noticed for six hours.

How long should you run in parallel?

Long enough to have observed every cycle the business runs on, which sets a floor no amount of resourcing can compress: at minimum one month-end, and for anything touching annual processes such as renewals, statements, tax or year-end close, one instance of that too. Then long enough to satisfy the exit criterion, which should be written before the phase begins.

State the criterion in the form interviewers want to hear: every high-value transaction type observed at least once, every difference class either resolved or accepted with a recorded reason and an owner, no new unknown class within a defined recent window, and coverage of the cycles. Then name the pressure you will face, which is that parallel running looks like inactivity to a sponsor watching a plan, and the counter is to report on the criterion's progress rather than on elapsed time, so the phase has visible burndown of its own.

Scope, retirement and dead code

How do you decide what not to migrate?

By making disposition an explicit decision per capability rather than a default. Migrate as is, migrate simplified, replace with a manual process, retire outright, or freeze the old system for that one path. Every capability gets one of those five, with a reason, and the exercise typically removes a meaningful fraction of the programme before any code is written.

The evidence bar is what interviewers probe, so volunteer it. Retirement requires telemetry over a full business cycle plus a search for callers outside the codebase plus a conversation with support, because "nobody uses it" is a claim and the support team will know about the one customer. And freezing deserves a mention as the underrated option: keeping a minimal read-only instance for a rarely-used but essential path is often better than rebuilding it, provided the ongoing cost is stated and the freeze has a review date rather than becoming a liability nobody remembers agreeing to.

How do you safely delete code you believe is dead?

Instrument, wait, search, announce, disable, then delete. Put a cheap always-on counter at the entry point and deploy it. Wait a full business cycle including the annual events. Search outside the codebase, which means database triggers and views, the job scheduler, other repositories, the API gateway's routes, and anything published to partners. Announce it somewhere the affected people will actually see, because the analyst who runs the year-end extract does not read the engineering channel. Disable it behind a flag or routing rule that can be reverted in seconds, wait again, and only then delete.

Then name the related category that is more dangerous: code that runs and has no effect. An event handler that has been empty since 2019, a validation whose result is discarded, a retry against a host decommissioned two years ago. These are not dead, and the risk is that somebody upstream believes they are working. Finding one during discovery is usually more valuable than the deletion itself.

Somebody objects to a retirement two weeks before the date. How do you handle it?

Take the objection seriously and make it specific, because the useful question is what they do with it and how often, not whether they want it. Frequently the answer reveals a much cheaper accommodation than keeping the capability: a quarterly extract instead of a live screen, a report from the warehouse instead of the application, a manual process for four cases a year, or a read-only archive.

What is being graded is whether you treat the objection as data about incomplete discovery rather than as an obstacle. One late objection means your telemetry and your announcement did not reach someone, and the response is both to accommodate them and to check whether the same gap hides others. What loses the question is either proceeding over the objection because the date is fixed, or capitulating and keeping the whole capability because one person spoke, since both skip the step where you find out what they actually need.

Programme, business case and people

How would you sequence a three-year modernisation so it survives a budget cut?

By ensuring it is valuable at every point at which it could be stopped, which is a design constraint on the plan rather than a reporting preference. Choose the first slice for what it teaches, not for what it is worth: small enough to finish in weeks, real enough that its lessons transfer, and connected enough that it exercises routing, data, deployment and reconciliation end to end. Decommission something small early, because a retired component is unambiguous progress a sponsor can defend. Draw the dependency graph on the data rather than on the code, since separability determines the order. And deliver a capability the business asked for within two quarters, because a programme with an advocate outside engineering survives things that one without does not.

Name the two anti-patterns explicitly. Platform-first, where a year is spent building tooling for migrations that have not started, so its assumptions are never tested against a real workload. And easy-slices-first, where the burndown looks excellent for a year and the programme then meets every hard problem at once with its remaining budget. Take at least one genuinely hard thing early so the estimates for the rest are grounded.

How do you report progress on something whose deliverable is that a system stops existing?

By measuring the removal rather than the addition, because teams deliver what is measured. Components decommissioned, legacy infrastructure and licence spend eliminated, manual operational steps retired, legacy code removed, and the proportion of traffic no longer touching the old system. A programme measured on components migrated will migrate components and leave the originals running.

Accept, and say, that this makes early progress look slower, and pair it with a leading indicator so the first six months are not a blank: change lead time on the affected area, incident rate, the number of people who can safely change each component. The credibility move is to publish the measures at the start and not change them, because a programme that redefines its metric in month fourteen has told everyone what happened.

Make the case for a modernisation to a board that thinks the current system works fine.

Concede the premise immediately, because it is true and arguing with it costs you the room: the system works, and that is why it is still there. Then locate the case in one of four currencies. Cost, meaning run cost, licence and the maintenance effort that would be released. Time to market, ideally a change the sponsor has personally waited six months for. Risk, expressed with a probability and a consequence in the language the risk function already uses. Constraint, meaning a product, market or deal that cannot be pursued at all today.

Then be honest about the shape: costs are near-term and estimable, benefits are later and less certain, there is a trough in the middle where two systems run and costs exceed both endpoints, and the estimate will be wrong in a known direction. Present three sized options with consequences rather than one proposal, and include the do-nothing option honestly costed, including the risk being carried. A board asked to decide between options engages; a board asked to approve a single number looks for reasons to say no.

The right answer is "do nothing yet". How do you defend that without sounding like you are dodging the work?

By making it a decision with structure rather than an absence of one. State the conditions that make deferral correct: stable system, supported substrate, low change rate, no blocked capability, and a better return available for the same capital elsewhere. Then attach the three things that turn a deferral into a plan. Concrete revisit triggers, expressed as events rather than sentiment, such as the extended support date, a change rate above a threshold, or the departure of the second-to-last person who understands it. A review date in the calendar with an owner. And the cheap mitigations done now, since documenting the system, adding observability, verifying a restore and reducing the key-person dependency cost a fraction of a modernisation and remove most of the near-term danger.

Finally, write it down as a decision record with its reasoning and its triggers, because in two years somebody will ask why nothing was done, and the difference between a considered decision and negligence is entirely the record. The mechanics of that, including what to do when the decision later has to be reversed by people who were not there, are in reversing an ADR when its authors have left.

One engineer understands the system and the modernisation depends on them. How do you handle it?

Treat it as a continuity risk with named mitigations rather than as a personality problem, and say the business version out loud: a single point of human failure in a business-critical system would be unacceptable if it were a server. The mitigations are pairing on real changes rather than knowledge-transfer meetings, documentation written by them and verified by someone else attempting the task from it alone, a second named person for each critical area, and a rotation on support for that system.

The half that candidates miss is their side of it. They have accumulated knowledge that is valuable only while the system exists, they have probably watched two previous attempts start and stop, and a programme that describes their work as debt will get compliance rather than cooperation. Make them the author of the replacement's domain knowledge rather than an interviewee, be explicit and early about their role afterwards, and frame the reduction of their singularity as something that lets them take a holiday. Interviewers ask this because a candidate who talks about the person as a bus-factor number and not as a colleague will fail at exactly the point where the programme needs goodwill.

The business keeps requesting changes to the system you are replacing. What do you do?

Refuse the framing that it is a freeze-or-not question, because a freeze will not hold and pretending otherwise makes you unreliable. Propose a change budget: changes continue, they are prioritised jointly with the modernisation work, and any change touching a slice currently being replaced is costed at double because it must be implemented twice. Making the double cost visible at the point of request converts an engineering complaint into a business decision, which is where it belongs, and it reduces the change volume more effectively than any policy.

Add the operational half. Keep patching the legacy system, keep it observable, keep the restores verified, and keep the runbooks current, because a legacy system that fails badly in month nine will consume the programme. Freeze structural investment, not operability. The wider allocation argument is in how much capacity goes to roadmap versus keeping the lights on.

Integration and the boundary

What is an anti-corruption layer protecting you from, exactly?

From the legacy model becoming the new system's model, which happens by default and not by decision. The mechanism is that the old system returns records with overloaded fields, undocumented status codes and meanings that depend on the record's age, and the path of least resistance is to pass those inward, at which point the new service is a tidier copy of the old design and the modernisation has produced nothing structural.

Describe the boundary concretely: nothing legacy-shaped crosses it, translation resolves every ambiguity explicitly in code somebody can read, and the translator returns either a valid domain object or an explicit error, never a partially populated object with nulls standing in for values it could not map. Then name the cost before the interviewer does, which is that the layer is maintained for as long as the legacy system lives and adds no feature, so it is right for a long-lived boundary or a genuinely toxic model and over-engineering for one endpoint against a system being retired in four months.

What do you do when the legacy system's data is ambiguous and nobody knows the answer?

Resolve it at the boundary, explicitly, with the evidence recorded, and prefer raising over defaulting. A blank field that means one thing before a migration date and another after it becomes two branches with a comment saying how you established the split, typically by counting rows on either side and confirming with the operations team. An unmapped value raises rather than defaulting, because a default silently converts unknown data into confident wrong answers and in a migration the unknown values are exactly the ones you need to see.

The signal is treating the mapping code as the durable artefact of the programme. It is the only place where twenty years of accumulated institutional knowledge gets written in a form that is executable, testable and reviewable, and it frequently outlasts the migration that produced it.

A downstream system depends on a quirk of the old system's output. Do you preserve it?

Yes, initially, and then negotiate it separately. Preserving it keeps the migration a migration; fixing it in flight makes the migration also a behaviour change, so any difference the downstream team observes is now ambiguous between the two, which is exactly what you cannot afford during a cutover.

Then say how you retire it, because leaving it forever is not the answer either. Characterise it, document that it is a known deviation with a reason, tell the downstream team it exists and that it is temporary, agree a date after cutover, and give them a way to opt into the corrected behaviour first if they can. Being able to hold "preserve now, fix deliberately later" as a sequence rather than a contradiction is the judgement this question is testing, and the contract mechanics for agreeing it are the substance of contract testing versus integration testing.

Should the new service call the legacy system synchronously, or should the data be replicated?

It depends on what the coupling costs you, and the answer should name both consequences rather than picking a side. A synchronous call means the new service's availability is bounded by the legacy system's and its latency includes the legacy system's worst case, which is often a batch-shaped system with occasional multi-second responses. Replication means a consistency window and two copies that can disagree, plus the obligation to build and monitor the propagation.

The deciding question is whether the read must be current for correctness. Balance checks, entitlement checks and anything with a financial or safety consequence generally must be, so accept the coupling and design around it with timeouts, circuit breakers and a defined degraded behaviour. Display data, search, reporting and enrichment generally need not be, so replicate and treat staleness as a stated property. Say which you would choose and what you would monitor to know if you chose wrong, since that last clause is what separates a preference from a design, and the underlying trade is the one in synchronous call or event, and what each couples.

Substrate, vendors and constraints

Your vendor has announced end of life in fourteen months. What are your first three moves?

Establish which date it is, because vendors publish several and they are not equivalent: end of sale, end of standard support, end of extended support, and the end of life of the hosting or hardware underneath. The date your risk function cares about is usually the security patch cut-off, and the gap between the announced date and the real one is frequently a year of extended support you can buy. Buying time is a legitimate strategy, not an evasion, and comparing its cost against the cost of a rushed migration is the analysis you are there to do.

Then establish the blast radius, because the product almost certainly serves several capabilities with different criticality, and it is common to find that two need replacing, three can be retired, two move to something you already own and one can be frozen. That analysis turns an unaffordable programme into an affordable one. Third, notice what the deadline does to strategy: a fixed external date makes scope the variable, which argues for a smaller target and a willingness to accept a temporary compromise, such as running the replacement against a legacy-shaped model until after the date. A vendor in your critical path announces end of life works through the negotiation.

You are asked about a mainframe modernisation and you have never worked on one. What do you say?

Say what you know, say what you would need to establish, and say who you would ask, in that order, because the interviewer is grading reasoning under an unfamiliar constraint set rather than testing recall. What you can say with confidence is that these systems are usually fast and reliable at their designed workload, that the drivers are typically people, cost, change speed and the awkwardness of real-time requirements against a batch-shaped design, and that the strategy set is the same one as anywhere else in specific forms: encapsulate behind a service interface, translate the source automatically, re-host the workload, or rewrite the logic.

Then name what you would need: the batch window and what happens when it is missed, transaction volumes and their profile, the file interfaces with their record layouts and encodings, the licensing model, and an honest count of people who can still change the code, because that last number paces everything. Add the two technical traps you can defend: decimal rather than binary floating point arithmetic, since a replacement computing money in a float produces small unacceptable differences, and character encoding and fixed-width record layouts as a real source of migration defects. That answer is markedly better received than confident terminology, and interviewers use the question precisely to find out which you will offer.

The batch window is six hours and your replacement takes eight. What do you do?

Treat it as a design invalidation rather than a tuning task, and say so, because that reframing is the answer. A missed batch window in most of these environments means the next day's online processing starts on stale or incomplete data, so it is a correctness and availability problem rather than a performance annoyance, and discovering it after cutover is discovering it at the worst possible time.

Then work the options in order of how much they change. Establish whether the work is genuinely serial or whether the dependency graph allows parallelism, because legacy batch is frequently sequential for historical reasons rather than logical ones. Establish whether the whole population needs processing or whether incremental processing of what changed is possible, which is usually the largest available win and is usually blocked by the old model not recording change. Establish whether the window itself is negotiable, since it is often set by a downstream consumer's expectation rather than by a hard constraint, and asking is free. Only then optimise. And say what you would do if none of that closes the gap, which is to change the design rather than to ship and hope, because a system that fits the window on the quietest day of the year does not fit it.

Judgement and behavioural rounds

Tell me about a modernisation you worked on that did not go well.

Answer with a specific mechanism rather than a general regret, because the mechanism is what demonstrates you learned something transferable. Strong material includes discovering a consumer nobody knew about after cutover, a reconciliation whose noise made it unreadable so the team stopped looking, a decommissioning that never happened so the estate got more expensive, a first slice chosen for ease so the estimates for the rest were worthless, or a scope that grew because the migration became a redesign.

What is graded is whether you own a decision rather than describing a circumstance. "The programme lost its sponsor" is a thing that happened to you. "We spent eleven months before anything was decommissioned, so when the sponsor changed there was nothing to point at, and I would now insist on retiring something small in the first quarter even if it is not the most valuable thing" is a decision, a consequence and a changed default. Interviewers are listening for the third clause.

The team wants to rewrite and you think they should not. How do you handle that?

Take the motivation seriously before addressing the proposal, because the desire is usually evidence of something real: they are afraid to change the system, releases hurt, they have no test feedback, or they are bored and worried about their skills. Each of those has a cheaper remedy than a rewrite and each is a legitimate problem, so the conversation goes better if you name what they are experiencing before you decline what they are asking for.

Then make the decision on evidence rather than authority. Ask what specific change is hard and work one end to end, ask what the business capability is that a rewrite would unlock, and put both options on the same page with their costs, risks and time to first value. Frequently the outcome is a middle path: a genuine investment in tests, deployment and observability, plus a strangling of the worst component, which addresses most of what the team wanted at a fraction of the cost. What loses the interview is either overruling without engaging or approving to keep people happy, since both skip the diagnosis. The dynamics are worked through in the team wants to rewrite a service that works.

How do you keep architecture knowledge from evaporating again once you have recovered it?

By putting it where the work happens rather than where documents go to die. The mapping code in the anti-corruption layer, comments recording how a fact was established, decision records for the choices with a reason and a trigger for revisiting, tests that encode behaviour, and a short living context document that is reviewed on a cadence with an owner rather than written once. The test that matters is whether a new engineer can find the answer without asking a person, and the way to check it is to have one try.

Say the honest part too, which is that documentation decays in proportion to its distance from the code and its lack of an owner, so the durable artefacts are the executable ones. A diagram nobody maintains is worse than no diagram because it is believed, which is the argument in keeping architecture documentation from going stale. The one place to spend effort on prose rather than code is the reasoning, because reasoning is the thing code cannot record and the thing whose absence made this system legacy in the first place.

What would make you stop a modernisation programme you were leading?

Name conditions rather than feelings, because the willingness to stop is only credible if it has criteria. The rate of divergence exceeding the rate of convergence, so the arithmetic says it cannot finish. The disappearance of the business driver, meaning the capability it was going to unlock is no longer wanted. Nothing decommissioned after a defined period, which indicates the programme is adding rather than replacing. A discovery that invalidates the central assumption, such as data that cannot be separated where the plan required it to be. Or a sponsor change that removes the multi-year commitment the approach depends on.

Then say what stopping means, because stopping well is a skill. Harvest what has value, which is usually the tests, the characterisation corpus, the documented domain knowledge and any component already cut over and retired. Return the rest to a maintainable state rather than leaving half-built scaffolding, since abandoned partial migrations are the origin of the next generation's legacy. And write down what was learned and why it stopped, so the organisation's third attempt starts from evidence rather than from enthusiasm.

How to prepare without memorising

The pattern vocabulary is worth an hour and no more. What repays effort is the sequence, because almost every question in this area is answered by placing it on the same path: understand from the running system, characterise before changing, seam before extracting, one slice at a time, parallel run before cutting, reconcile before deciding, decommission before starting the next. Practise walking that out loud with a failure and a control at each stage, and most unfamiliar scenarios become a question of which stage you are being asked about.

Attach to each stage the detail that surprises people. Evidence comes from production and not from documents. A characterisation test asserts the bug. The first edit that creates a seam is unverified by definition, so it must be reviewable by eye. A strangler programme is measured by what it retires. Reconciliation output has to be classified or it becomes noise nobody reads. Rollback ends when the old system stops receiving writes. The batch layer sets a floor on the calendar that resourcing cannot move. And a default in a translation layer converts unknown data into confident wrong answers.

Then rehearse the habits, because they are cheaper to acquire than knowledge and they are what interviewers actually observe. Ask what is known before proposing anything. Sequence stabilisation before structure. Name where rollback ends and who owns crossing it. Say which parts of the estate you would retire rather than migrate. State the unknowns and what it would cost to resolve them. And be willing to say that the right answer this year is to do nothing yet, with triggers and a review date attached, because the candidates who can defend that are the ones who have been trusted with the decision before.