Legacy Modernization and Architecture Evolution Questions
Evolving an existing system rather than designing greenfield. Covers the modernization patterns (strangler fig, anti-corruption layers, facades and protocol adapters), choosing between rehosting, replatforming, incremental refactoring and a full rewrite, data migration and coexistence (dual-running, change-data-capture versus bulk cutover, reconciliation and drift), cutover readiness and decommissioning, recovering undocumented behavior from legacy code and stored procedures, instrumenting a migration so you can tell in real time whether it is working, and the organizational and risk management of long migrations across many teams. The scope is the migration itself: not quantifying or prioritizing technical debt, not code-level refactoring craft, not how to decompose a system into microservices, and not cloud migration or deployment and rollback mechanics as topics in their own right.
Strangler fig, anti-corruption layer, facade, and a full rewrite all show up in conversations about modernizing a legacy system, and interviewers often use them loosely. How do you decide which one actually fits a given situation, and what makes you abandon the incremental approach partway through?
Sample Answer
Direct answer
These are four different tools for four different situations, and interviewers who use them interchangeably are usually testing whether you actually know the difference. A strangler fig is for gradually replacing a legacy system's functionality behind a routing seam while it stays live. An anti-corruption layer is for protecting a new system's domain model when it has to talk to a legacy system it is not replacing (or not replacing yet). A facade is for simplifying and unifying how callers interact with a messy legacy system, without migrating anything or translating between two different domain models at all. A full rewrite is for when the legacy system cannot be safely peeled apart at all. The decision comes down to whether the system can be decomposed into independently extractable pieces, and whether it needs to keep running the whole time. A fourth axis matters just as much: whether you are actually trying to replace anything at all, or you just want a safer, simpler interface onto a legacy system you have no plan to migrate away from, which is what a facade alone is for.
Structured elaboration
A useful way to separate them:
- Strangler fig answers "how do I replace this system's functionality over time without a big cutover." It assumes the system can be broken into pieces that can move independently, and it is a migration strategy, not a permanent architecture. Use it when the legacy system is large but decomposable, and downtime is not acceptable.
- Anti-corruption layer answers "how do I integrate with this system without its bad decisions becoming my bad decisions." It does not assume you are replacing anything; you might be integrating with a legacy system permanently (a partner's system you do not control) or temporarily (as one piece of a larger strangler effort, where the ACL sits between the parts still on legacy and the parts already moved). Use it any time a system you do not fully trust the shape of has to feed a system whose domain model you want to keep clean.
- Facade answers "how do I make a messy legacy system safer and simpler to call, without replacing or translating anything." Unlike an anti-corruption layer, it does not have to reconcile two different domain models, it is a single unified interface placed in front of a system you are not migrating away from, at least not yet, so callers stop depending directly on the legacy system's tangled internals. Use it when the goal is purely to make what already exists safer to call, or as the seam a later strangler-fig effort will route traffic through once you do decide to replace what is behind it.
- Full rewrite answers "the incremental approach is not viable here." Use it when the legacy system's capabilities are so tightly coupled that there is no seam to strangle along, when the code is small enough that a rewrite is genuinely cheaper than untangling it, or when the business can tolerate a real code freeze while the rewrite happens. It is also sometimes the right call for build-versus-buy reasons that have nothing to do with technical coupling: if a vendor product now does what the core subsystem does, replacing rather than incrementally modernizing can be the faster and cheaper path, provided the migration and switching costs are honestly priced in.
In practice they combine rather than compete: a strangler-fig migration typically starts by placing a facade in front of the legacy system to create a single seam, then uses an anti-corruption layer at that seam to protect the already-migrated parts from the still-legacy parts as pieces move across it.
Worked example
A team replacing a core subsystem (say, pricing) weighs the options (a facade alone is ruled out early, since the goal is to actually move pricing off the legacy system, not just make it safer to call):
- Strangler: pricing logic touches a dozen call sites across the codebase, but each call site is independently identifiable, so they can move pricing behind a facade and migrate call sites one at a time. This is the default choice given decomposability.
- ACL alone (no strangler): they decide pricing itself will stay on the legacy system for now, but a new checkout service needs pricing data. Rather than have checkout speak the legacy pricing format, they add an ACL so checkout's domain model stays clean, with no plan yet to replace pricing itself.
- Full rewrite / buy: they discover pricing logic is deeply entangled with tax and discount logic in ways that resist any clean extraction, and a commercial pricing engine now covers the requirements. They rewrite (replace) rather than strangle, accepting a scoped migration project with a defined cutover instead of an open-ended incremental one.
What would make the team abort a strangler approach midway and fall back to one of the other two: discovering that the "independent" call sites actually share hidden mutable state that makes partial migration unsafe, or finding the timeline slipping so far that the cost of running two systems is exceeding the cost a rewrite would have been from the start.
Trade-offs and pitfalls
The trap is picking strangler fig by default because it feels lower-risk, without checking that the system is actually decomposable; forcing a strangler approach onto tightly coupled logic produces years of a half-migrated system with all the maintenance cost of two systems and none of the safety benefit, because the seam itself becomes unreliable. The opposite trap is reaching for a full rewrite out of frustration with legacy code, when a narrower ACL would have solved the actual integration problem at a fraction of the cost and risk.
When is a full rewrite of a legacy system actually the right call instead of an incremental refactor, and what has to go right for it to work? Walk through the risks a rewrite introduces that an incremental approach avoids, and vice versa.
Sample Answer
Direct answer
A full rewrite wins when the legacy system's structure fights every incremental change so hard that the accumulated cost of working around it, in time, in bugs, in the parts of the team's attention it consumes, exceeds a realistic (padded) estimate for building it again from what you now know. What has to go right: the team genuinely understands the system's current behavior well enough to not silently drop requirements, the business can tolerate a real delivery gap, and there's a credible plan for the parts of the estimate that are hardest to predict, data migration and the long tail of edge cases nobody remembers exist until they break in production.
Structured elaboration
The risks a rewrite introduces that incremental work avoids:
- The estimate is a guess dressed as a plan. You cannot fully know what a legacy system does until you've read every path through it, and if you could do that cheaply, you probably wouldn't need a rewrite. Rewrites reliably underestimate the "long tail," the 20% of behavior that's undocumented, weird, and only matters for edge cases, because that's exactly the part that's hardest to discover in advance.
- All-or-nothing delivery. An incremental effort can stop, ship partial value, and reassess. A rewrite typically can't ship real value until most of it is done, which means the business is exposed to the full cost of the effort before seeing any of the benefit, and a rewrite that stalls at 80% delivers zero value for the investment made.
- Data migration risk concentrates at the end. Incremental approaches migrate data piece by piece, catching problems early on a small blast radius. A rewrite often defers data migration to a single cutover at the very end, which is exactly when the team has the least remaining slack to absorb a surprise.
The risks incremental work introduces that a rewrite avoids:
- Running two systems for longer than planned, with the ongoing cost and the risk of the migration simply never finishing.
- The seam itself becoming a source of bugs, translation errors at the boundary between old and new that a from-scratch rewrite wouldn't have to deal with.
- Slower overall delivery of the target end state, since incremental work is deliberately paced to be safe rather than fast.
Concrete mitigations for the rewrite risks: build characterization tests against the legacy system's actual behavior before writing a line of the replacement, so the estimate is grounded in observed behavior rather than assumed behavior; plan the data migration and cutover as a first-class, staged piece of the project rather than a final step; and set an internal checkpoint (not a public commitment) partway through where the team honestly reassesses whether the estimate is holding, with permission to fall back to a hybrid approach if it isn't.
Worked example
A retrospective example: a team decided a legacy inventory system's coupling to a proprietary rules engine made incremental extraction impractical, so they chose a rewrite. What made it work: they spent the first month purely on characterization testing against the legacy system, capturing behavior for every product category and edge case they could enumerate, before writing any new code. That testing surfaced a rounding rule for one product category that looked like a bug but turned out to be a deliberate accommodation for a regulatory requirement in one region, exactly the kind of hidden logic a rewrite risks silently dropping. Because they'd captured it in a test before starting, the new system preserved it correctly, and the team could point to a passing test suite, not just confidence, when they cut over.
An architectural decision that limited scale, brought up in the same retrospective: the original system had used a single shared table for all regions' inventory data, which made the regulatory rounding rule and a dozen similar region-specific rules invisible in the schema and discoverable only by reading application code path by path. The rewrite's replacement schema made region-specific rules an explicit, first-class concept, directly because the team had been burned by how hard the old shape made those rules to find.
Trade-offs and pitfalls
The single biggest risk-reducer for a rewrite is treating "we understand the current behavior" as a deliverable to prove, via characterization tests against real behavior, rather than an assumption to proceed on. Teams that skip this step and start writing new code based on what they believe the system does, rather than what it's actually observed to do, are the ones whose rewrites silently change behavior and discover it in production months later.
Design a decommissioning plan for shutting down a legacy system after its replacement has taken over. What has to be true before you actually delete anything?
Sample Answer
Direct answer
Before you delete anything, you need proof the migration actually succeeded (not just that the new system is live), a defined retention and archival plan for whatever legal or audit obligations outlive the system itself, a rollback path in case something surfaces after decommission that the pre-cutover testing missed, and a communication plan that reaches every stakeholder who might still depend on the old system, including the ones you do not already know about.
Structured elaboration
A decommissioning plan has four parts, and skipping any of them is how "the migration is done" turns into "we deleted data we needed":
- Verification that the migration is actually complete. Not "the new system works," but "nothing depends on the old one anymore." This means auditing traffic and access logs on the legacy system for weeks after the cutover, not just at the moment of cutover, because low-frequency dependencies (a monthly batch job, a quarterly report) will not show up in a one-week traffic sample.
- Legal and audit retention requirements. Many systems have data that has to remain queryable for years after the system itself is gone, for regulatory or contractual reasons. That means an archival strategy decided before decommission, not scrambled together after someone asks for five-year-old records the week after you deleted the database.
- A rollback plan for the decommission itself, distinct from the rollback plan for the original migration. If something surfaces after you have shut the legacy system down (a caller nobody knew about, a data discrepancy only visible under a rare condition), you need a defined path back, even if that path is "restore from the last verified backup and re-enable the legacy code path for a bounded window," not "we have no idea, we deleted it."
- Third-party integrations and stakeholder communication. External partners often integrate with systems in ways your internal traffic logs cannot see (a partner polling an API you exposed to them specifically). The communication plan needs to reach them with enough lead time to migrate on their side, not just notify internal teams.
Worked example
A team decommissioning a legacy order-management system after a successful migration:
- They keep the legacy system read-only (not deleted) for 90 days post-cutover, monitoring access logs the whole time. In week six, they find a quarterly compliance report job still reading directly from the legacy database, which nobody had flagged as a dependency because it only runs four times a year.
- They export the full historical dataset to a queryable archive with a retention period matching the company's seven-year audit requirement, and verify a sample of archived records against the live system before the live system goes away, since an archive nobody has tested is not actually a safety net.
- They notify the three external partners who integrate with the legacy system's API directly, giving them 60 days' notice and a migration guide, rather than assuming internal migration alone covers everyone with a dependency.
- Only after all of this, and after a final confirmed zero-traffic week on the legacy system, do they actually shut it down, with the archived data and a documented restore procedure kept in case something surfaces later.
Trade-offs and pitfalls
The tempting shortcut is to declare victory the moment the new system handles 100% of live traffic and decommission immediately, but "no traffic this week" is not the same as "no dependencies," and the cost of being wrong (deleted data you needed, a partner integration silently broken) is far higher than the cost of a monitored grace period before deletion. The other common mistake is treating archival as a technical afterthought rather than a compliance requirement with its own sign-off, which is how companies end up unable to produce records a regulator or auditor asks for.
A legacy service is generating enough production pain (frequent incidents, slow releases, brittle deploys) that something has to change, but you cannot stop shipping features to fix it properly. How do you sequence the work?
Sample Answer
Direct answer
When a legacy service is generating enough operational pain that something has to change, but the business can't absorb a full stop on feature work, the answer is to run both in parallel deliberately: carve out a defined, protected slice of engineering capacity for modernization work while feature work continues on everything else, rather than treating it as something the team squeezes in during slack time that never actually materializes.
Structured elaboration
- Diagnose before allocating. Understand what's actually driving the incident volume (a specific fragile subsystem, a category of bug, an operational gap like missing monitoring) before deciding what modernization work would actually reduce it, so the effort targets the real cause rather than a plausible-sounding one.
- Balance short-term fixes and long-term work explicitly, as two named tracks. Short-term operational fixes (better alerting, a faster rollback path, patching the specific recurring bug) reduce pain quickly and buy the credibility and breathing room to invest in the longer-term structural fix. Skipping straight to the long-term fix without the short-term relief usually means the incident volume stays high long enough to erode stakeholder patience before the real fix lands.
- Allocate a protected percentage of capacity, not "whatever's left over." A common and defensible pattern is a fixed percentage of each sprint or quarter dedicated to modernization work, protected from being silently reabsorbed into feature work when a deadline looms, because that reabsorption is exactly how "we'll get to it" becomes "we never got to it."
- Define milestones and track metrics that show progress, not just effort spent: incident volume trending down, time-to-resolve improving, the specific fragile subsystem's change-failure rate improving. Without a visible metric, it's hard to defend the ongoing capacity allocation against pressure to redirect it entirely to features.
- Sequence quick wins first. For a system with multiple problems, addressing the ones that reduce risk or cost the most relative to effort first builds momentum and stakeholder trust in the approach, which matters for sustaining the capacity allocation over the following months.
Worked example
A legacy service generating a high volume of production incidents that teams repeatedly patch without addressing the root cause:
- Diagnosis reveals the majority of incidents trace back to a single fragile module with no automated tests and a history of being modified under time pressure without review.
- Short-term track: the team adds targeted monitoring and a faster, safer rollback path for that specific module immediately, cutting incident resolution time even before any structural change, and buying visible relief that reduces pressure while the longer effort proceeds.
- Long-term track: 20% of each sprint's capacity is protected for incrementally adding test coverage and refactoring the fragile module, with an explicit agreement from leadership that this allocation survives normal sprint-planning pressure rather than being the first thing cut when a deadline is tight.
- Milestones: the team tracks incident count attributable to this specific module monthly, targeting a 50% reduction within two quarters, a concrete, visible number that justifies the ongoing capacity allocation to stakeholders who are not tracking the work day to day.
- Six months in, incident volume from the targeted module has dropped substantially, which the team uses as evidence to negotiate continued (or expanded) protected capacity for the next fragile area, rather than the effort quietly winding down once the initial crisis passed.
Trade-offs and pitfalls
The trade-off is slower feature delivery in the near term against a system that stops generating enough operational pain to keep eating unplanned time regardless; teams that skip this trade and try to do modernization work purely in slack time consistently find that slack time never materializes under real delivery pressure, and the work simply doesn't happen. The most common pitfall is a protected-capacity allocation that exists on paper but gets silently deprioritized the first time a real deadline conflicts with it, which is why tracking and publicizing the resulting metric improvement matters: it's the evidence that keeps leadership honoring the allocation the next time there's pressure to cut it.
A legacy codebase has slow, flaky tests and fragile infrastructure, and you're adding fast-moving new services alongside it. How do you keep pull-request feedback fast without giving up confidence that nothing broke?
Sample Answer
Direct answer
Keeping pull-request feedback fast while a slow, flaky legacy test suite sits alongside new, fast-moving microservices means being selective about what runs on every PR versus what runs less often: run only the tests actually affected by a given change on the fast path, push the full legacy suite to a separate, less frequent gate, and invest in making the infrastructure itself (caching, parallelization, image layering) work for you rather than accepting the legacy suite's slowness as a fixed cost every engineer pays on every change.
Structured elaboration
- Affected-test selection. Rather than running every test on every PR, determine which tests are actually relevant to the changed code (via dependency analysis or code-coverage mapping) and run only those on the fast path. For the new microservices, this is usually straightforward since they're smaller and more decoupled; for the legacy monolith, building accurate affected-test selection is harder because of its coupling, but even an imperfect heuristic (test files historically correlated with changes in a given legacy module) beats running the entire slow suite on every change.
- Caching aggressively. Dependency installation, build artifacts, and container image layers should be cached and reused across CI runs whenever the underlying inputs haven't changed, which disproportionately helps the legacy codebase's typically heavier build.
- Parallelization. Split the test suite (both the fast, affected-test subset and the full suite when it does run) across multiple workers, so wall-clock time drops even when total test time doesn't.
- Container image layering. Structure the build so that layers which rarely change (base OS, rarely-updated dependencies) are cached separately from layers that change on every commit (application code), so a typical PR only rebuilds the thin, fast-changing layer.
- A separate, less frequent gate for full integration coverage. Full integration tests, including the slow and historically flaky legacy ones, run on merges to main rather than on every PR, giving fast feedback for the common case while still catching integration issues before they reach production, just not on the critical path of every individual change.
- Reliability for long-running tests specifically. Flaky legacy tests undermine trust in the whole CI system if they're allowed to block PRs; quarantining known-flaky tests (tracked and fixed on their own timeline, not ignored forever) out of the blocking path, while still running them and flagging failures for investigation, keeps the fast path fast without silently losing coverage.
Worked example
A team with a legacy monolith (slow tests, fragile infra) alongside new microservices, targeting under 10 minutes for most PR feedback:
- Affected-test selection: a dependency graph built from the legacy codebase's import structure identifies roughly which legacy test files are relevant to a given changed module; it's imprecise (occasionally over-selects tests unrelated to the actual change) but cuts the legacy tests run on a typical PR from the full 45-minute suite to a relevant 4-minute subset.
- Caching and layering: the legacy monolith's dependency installation, previously taking 3 minutes on every CI run, drops to under 20 seconds once dependencies are cached and only reinstalled when the lockfile changes; container layers are similarly split so a typical code-only change doesn't rebuild the base image.
- Parallelization: the microservices' naturally smaller, faster test suites run fully on every PR in parallel across several workers, comfortably fitting the 10-minute target on their own.
- Full suite on merge to main: the complete legacy integration suite, including known-flaky tests that are tracked separately for fixing, runs on every merge to main rather than every PR, giving a safety net without slowing down the common case of an individual PR.
- Combined, a typical PR touching one microservice and a small piece of the legacy monolith now finishes CI in about 7 minutes, down from the previous roughly 48-minute full-suite run on every change (the 45-minute legacy suite plus the 3-minute uncached dependency install stated above, run serially with no affected-test selection or caching).
Trade-offs and pitfalls
The trade-off is a small amount of risk (an imperfect affected-test-selection heuristic occasionally missing a genuinely relevant test) against dramatically faster feedback for the common case, a trade that's almost always worth making as long as the full suite still runs somewhere before production, catching what the fast path might have missed. The pitfall is quarantining flaky tests and then never actually fixing them, which quietly erodes real coverage over time even though the CI dashboard looks green; a quarantine needs a tracked, revisited backlog, not a place tests go to be forgotten.
Unlock Full Question Bank
Get access to all 9 Legacy Modernization and Architecture Evolution interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.