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.
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.
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.
You discover that a chunk of the business's real logic lives in stored procedures inside a legacy database, undocumented and unowned. How do you find it, decide what matters, and get it out safely?
Sample Answer
Direct answer
Business logic hiding in stored procedures is one of the most common and most dangerous forms of legacy debt, because it is invisible to normal code review and version control, and it often encodes rules nobody remembers the reasoning behind. The approach is to discover every procedure and what actually calls it, document what each one really does (not what its name implies), prioritize by business risk and change frequency, and migrate the highest-priority ones first behind tests that pin down current behavior before you touch it.
Structured elaboration
- Discover. Query the database's system catalog for every stored procedure, trigger, and function, then cross-reference against application logs and query traces to find which ones are actually invoked in production and by what, since a database can accumulate procedures nobody calls anymore alongside ones that run on every request.
- Document. For each procedure that is actually used, read the logic and write down what it does in plain language, cross-checking against real data where the logic is ambiguous. Stored procedures are notorious for encoding business rules through side effects (a procedure that updates three unrelated tables because someone needed that behavior for a feature that no longer exists), so this step often surfaces rules that contradict what the current documentation or team's mental model says the system does.
- Prioritize. Rank by a combination of business criticality (does this touch money, compliance, or customer-facing correctness) and change frequency (procedures that get modified often are more urgent, both because they are more likely to be misunderstood and because they are more likely to need a modern development workflow around them). Deprioritize procedures that are stable, low-risk, and rarely touched, even if migrating them would be technically satisfying.
- Migrate with behavior preservation as the bar, not behavior improvement. Before rewriting a procedure's logic in application code, write characterization tests against the stored procedure itself, feeding it a range of real and edge-case inputs and recording its actual outputs. The new implementation has to match those outputs exactly, even if some of them look like bugs, because "fixing" what looks like a bug during a migration is how migrations silently change business behavior nobody signed off on changing.
- Cut over incrementally, one procedure at a time, keeping the old stored procedure in place and comparing outputs in production (or against a production data replay) before removing it, the same discipline as any other legacy migration.
Worked example
A team discovers a stored procedure called calc_shipping_fee that, per the discovery step, is called from 14 different places across the codebase and three external batch jobs nobody had documented.
- Reading the procedure's logic reveals it does not just calculate a shipping fee; it also silently applies a loyalty-program discount for a specific customer tier, a piece of business logic nobody on the current team knew existed, discovered only because the documentation step forced someone to read the actual SQL rather than trust the procedure's name.
- They prioritize it high: it is called frequently, touches revenue, and the undocumented discount behavior is exactly the kind of hidden rule that causes production incidents when someone eventually "cleans up" the procedure without realizing what it actually does.
- They write characterization tests feeding the procedure a range of order types, customer tiers, and edge cases (a zero-weight order, a customer at the loyalty threshold boundary), capturing its exact current output for each.
- They reimplement the logic in a new service, including the discount behavior even though nobody remembers why it exists (they flag it for a product decision separately, but they do not silently drop it during the migration), and validate the new service against the characterization tests plus a sample of real production traffic before cutting any of the 14 call sites over.
- They migrate call sites one at a time, keeping the stored procedure callable as a fallback until all 14 plus the 3 batch jobs have moved, then remove it.
Trade-offs and pitfalls
The core risk is discovering business logic you did not know existed and either silently dropping it (changing behavior nobody approved changing) or silently "fixing" it because it looks wrong (same problem, opposite direction). The discipline that prevents both is characterization testing before any reimplementation, treating "what does it currently do" and "what should it do" as two separate questions, and only asking the second one, with the right stakeholders, after the first one is answered honestly.
Explain the strangler fig pattern for retiring a legacy system incrementally. What are the most common ways teams get it wrong in practice, and what signals tell you strangling is the right call versus a full rewrite?
Sample Answer
Direct answer
Strangler fig retires a legacy system by growing a new one around it: you put a routing layer (a proxy, gateway, or facade) in front of the legacy system, move one capability at a time behind that layer to a new implementation, and let the old and new code paths coexist until every capability has moved and the legacy system can be turned off. The name comes from the strangler fig vine, which grows around a host tree until the host is no longer needed. It is popular precisely because it avoids the two failure modes of a big-bang rewrite: shipping nothing for a year, and cutting over everything at once with no way back.
Structured elaboration
The mechanics, in order:
- Put a seam in front of the legacy system. Usually an API gateway, reverse proxy, or a facade inside the monolith itself, so callers do not know or care whether a request lands on old or new code.
- Pick the first capability to extract, usually the one that is both low-risk and high-pain (a module that changes often but is not the most business-critical, so mistakes are cheap to learn from).
- Build the new implementation and route a slice of traffic to it, verifying its output matches the old path before trusting it fully.
- Repeat, capability by capability, until nothing is left behind the seam pointing at the legacy system.
- Decommission the legacy code only once nothing routes to it and you have confirmed there are no hidden callers.
The most common ways teams get this wrong, in rough order of how often they show up:
- Never actually finishing. The easy 80% of capabilities get strangled in the first few months, and the hard 20% (the ones with the messiest coupling to legacy state) get deferred indefinitely. Two years later the org is paying to run and secure both systems forever, which is worse than either a rewrite or leaving the legacy system alone. This is the single most common failure and the reason a strangler effort needs a decommission target with a rough date attached, not just a start.
- Building a permanent adapter instead of a temporary one. The seam is meant to shrink as capabilities move; if it keeps growing new special cases instead, it has quietly become a second system to maintain, not a migration path.
- Not enforcing a hard boundary between the shared state. If the new service and the legacy system both write to the same tables without a clear ownership rule, you get silent data corruption long before anyone notices a functional bug.
- Treating the seam as free. Every hop through a translation layer costs latency and adds a new thing that can fail; teams who never measure this get surprised when the "temporary" adapter becomes the slowest part of the system.
Strangling is usually the right call when the system has to keep serving traffic throughout the change (most production systems), when you can identify genuinely separable capabilities, and when the org can tolerate running two systems for a while. A full rewrite becomes more attractive when the legacy system's capabilities are too entangled to peel apart one at a time, when the business can tolerate a real code freeze, or when the legacy code is so far from correct that incrementally wrapping it just preserves its bugs behind a nicer API.
Worked example
Say a monolith handles catalog, cart, checkout, and recommendations for an e-commerce site. A team decides to strangle it:
- They put an API gateway in front of all four capabilities.
- They pick recommendations first: it changes often, has no write path into the order/payment data, and a bug there degrades the experience rather than losing money.
- They build a new recommendations service, route 5% of traffic to it behind a flag, compare its output to the legacy path for a few weeks, then ramp to 100% and delete the legacy recommendations code.
- They repeat for catalog, then cart, and leave checkout, the most state-heavy and highest-risk capability, for last, once the team has practiced the pattern three times on lower-stakes capabilities.
- Eighteen months in, nothing routes to the legacy monolith and it is decommissioned.
The order matters: doing checkout first, before the team has proven the pattern on anything, is exactly the kind of decision that produces the abandoned-halfway failure mode above.
Trade-offs and pitfalls
Strangling trades speed for safety: you ship value continuously and can stop or reverse at almost any point, but you pay for it in the ongoing cost of running two systems and maintaining the seam between them, and in the discipline required to actually finish rather than stall. A full rewrite is the opposite bet: faster in principle if nothing goes wrong, but an all-or-nothing wager on a fixed-price estimate for a system whose exact behavior nobody has fully mapped, which is exactly the situation legacy modernization starts from. The senior mistake to watch for is choosing strangling for the safety story and then never applying the same rigor to actually retiring the legacy code, which converts a migration strategy into permanent architectural debt.
A widely used internal tool has reached end of life and your team has to sunset it while everyone else still depends on it. How do you get people to actually move off it?
Sample Answer
Direct answer
Sunsetting a widely-used internal tool that's reached end of life without your own team's continued support means treating the other teams still depending on it as stakeholders you need to actively move, not just notify, which requires a real communication plan, a realistic timeline with margin, genuine incentives to migrate, and honest budgeting for both the sunset work and the disruption it causes the teams still relying on the tool.
Structured elaboration
- Communicate the decision and the why, early and repeatedly. Teams still depending on the tool need to hear the decision, the reasoning (why it's reached end of life, why continuing to support it isn't viable), and the timeline as early as possible, since the amount of lead time you give directly determines how smoothly other teams can actually plan their own migration work around it.
- A realistic migration timeline, not an aspirational one. Build in margin for the reality that other teams have their own priorities and won't necessarily treat migrating off your tool as urgent until the deadline is close, and plan communication checkpoints (not just a single announcement and a single deadline) to keep the migration visible on their radar throughout.
- Risk mitigation for the teams still on the old tool. If support is genuinely ending, what happens to a team that hasn't migrated by the deadline? A real risk mitigation plan names this explicitly (a hard cutoff with no further support, a limited best-effort extension for teams with a documented migration plan already underway, or something else), rather than leaving it ambiguous and discovering the answer only when the deadline actually arrives.
- Incentives to encourage migration, since asking teams to prioritize migrating off your tool competes directly against their own roadmap and incentives. Concrete incentives: dedicated migration support from your team for a limited window, a genuinely easier or better replacement that makes migrating a net win rather than a pure cost, or executive sponsorship that makes the deadline a real organizational priority rather than one team's internal decision that other teams can deprioritize.
- Budgeting and resourcing, both for your own team's sunset work and for the teams migrating off it. If teams need real engineering time to migrate, and that competes with their existing roadmap, the plan needs an honest accounting of that cost, ideally with a resourcing conversation happening at the same level (with the relevant managers or leadership) as the original decision to sunset the tool, not left as an unstated assumption.
Worked action framework
Sunsetting a widely-used internal ETL tool that's reached end of life:
- Stakeholder communication plan: an initial announcement six months before the planned sunset date, naming the reason (the tool's underlying dependency is itself end-of-life and a security risk to keep running), followed by monthly check-ins with each dependent team individually, not just broadcast announcements, tracking their specific migration progress.
- Migration timeline with margin: the announced deadline is set with a deliberate two-month buffer beyond the team's own internal target, based on past experience that dependent teams' actual migration pace tends to run behind their own initial estimates.
- Risk mitigation for the deadline: teams with a documented migration plan already in progress but not yet complete by the deadline get a defined, limited extension (30 days) with continued best-effort support; teams with no migration plan in progress by 60 days before the deadline get an explicit escalation to their leadership, making the risk visible before it becomes a crisis at the deadline itself.
- Incentives: the team offers dedicated migration office hours and a pre-built migration script covering the most common usage pattern, directly reducing the cost of migrating for teams that would otherwise have to figure it out from scratch, which meaningfully increases the number of teams that migrate proactively rather than waiting until the deadline forces them.
- Budgeting: the sunset effort itself (building the migration script, running office hours, maintaining the old tool through the transition window) is explicitly resourced as a project with allocated engineering time, rather than something the team is expected to absorb alongside its normal roadmap unfunded.
Trade-offs and pitfalls
The trade-off is the real cost of investing in migration tooling, incentives, and sustained communication against the alternative of a hard deadline with minimal support, which is cheaper for your own team in the short term but reliably produces a much worse outcome (teams caught unprepared, an escalation crisis right at the deadline, or a deadline that has to slip anyway because too many teams aren't ready). The pitfall to avoid is treating the initial announcement as sufficient communication; without sustained, individual check-ins, dependent teams reliably deprioritize a migration that isn't urgent yet, right up until it suddenly is, at which point there's no longer enough runway left to do it well.
Unlock Full Question Bank
Get access to all 22 Legacy Modernization and Architecture Evolution interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.