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.
You're leading a modernization initiative that touches multiple teams and stakeholders who do not report to you. How do you keep it moving, keep people aligned, and know early if it's at risk?
Sample Answer
Direct answer
Leading a modernization initiative across stakeholders who don't report to you means treating alignment as ongoing work with its own cadence and artifacts, not a single kickoff meeting, and building an explicit risk view (what could go wrong, mapped to business impact, with a plan if it does) so sponsors can make informed calls rather than being surprised later. Momentum comes from visible, regular progress and from making it easy for busy stakeholders to stay bought in without having to chase them.
Structured elaboration
- Identify what each stakeholder group actually needs, not just informs. Product cares about delivery risk to their roadmap, security cares about the compliance posture during and after migration, legal cares about contractual exposure, operations cares about who's on the hook when something breaks. A single generic update deck rarely serves all of them well; tailor what you surface to each.
- Cadence and artifacts. A regular (weekly or biweekly, depending on pace) short status update, focused on risk and decisions needed, not just activity, plus a living risk register that's actually referenced in conversations rather than filed away after being created once.
- Escalation paths, defined before you need them. When a decision or a blocker needs someone above your authority to resolve, who is that, and how quickly can you reach them? Waiting to figure this out during an actual crisis costs exactly the time that matters.
- A risk matrix as a concrete artifact: map likely technical and organizational risks to business impact and likelihood, with named mitigations and contingency plans, and a contingency budget attached to the risks that are both high-impact and hard to mitigate away entirely. This turns "modernization is risky" from a vague worry into something leadership can actually evaluate and fund appropriately.
- Leading indicators, not just lagging ones. Track signals that predict a risk materializing (slipping milestones, a spike in the fragile subsystem's incident rate) so you can escalate before the risk becomes a crisis, not after.
- Long-horizon initiatives specifically (a multi-year roadmap, phasing out legacy middleware) need incentive alignment: product teams asked to prioritize migration work over their own roadmap need a real reason to (a KPI they're measured on, a contractual deadline, executive sponsorship that makes it visibly a priority), not just a request for goodwill that competes with their own incentives every planning cycle.
Worked example
A team leading a cross-functional modernization initiative touching product, security, legal, and operations:
- Cadence: a biweekly 20-minute update to a standing stakeholder group, focused on three things: what shipped, what's at risk, and what decision (if any) is needed from this group before the next update. This respects busy stakeholders' time far better than a long status meeting nobody reads the prep for.
- Risk matrix: technical risks (a specific legacy dependency turning out to be more entangled than expected) and organizational risks (a key engineer leaving mid-migration) are both mapped to business impact and likelihood, with named mitigations, and a contingency budget is set aside specifically for the technical risks judged both high-impact and hard to fully de-risk in advance.
- Escalation: when the migration team and the security team disagree about an acceptable interim compliance posture during the transition, the pre-defined escalation path (a joint decision from the engineering director and the security lead, with a 48-hour SLA for a decision) resolves it in two days instead of the weeks it might have taken without a defined path.
- Sustaining momentum on a multi-year effort: for the legacy middleware phase-out specifically, the team ties migration progress to a KPI that product teams are already measured on (reduced support-ticket volume from a specific class of legacy-related bug), so migrating isn't competing against their own incentives, it's aligned with them.
Trade-offs and pitfalls
The trade-off is the overhead of maintaining regular cadence and a living risk register against the cost of stakeholders discovering a risk only once it's already a crisis; for anything spanning multiple teams and quarters, that overhead is worth paying deliberately rather than accidentally. The pitfall to watch for is a risk register that's created once at kickoff and never revisited, which gives the appearance of risk management without the substance, since the risks that actually matter six months into a long initiative are rarely the same ones identified at the start.
After migrating a table to a new service, you start seeing mismatches between the old and new systems because they disagreed on something basic (nullability, formatting, or a similar constraint). How do you detect that kind of divergence at scale and fix it without a full re-migration?
Sample Answer
Direct answer
Detecting divergence caused by a constraint mismatch (the legacy system allowed something the new one doesn't) means comparing the two systems at the field level, not just checking that records exist in both, and repairing it means writing an idempotent, safe-to-re-run fix rather than a one-off manual patch, because you'll likely need to run it more than once as new instances of the same problem surface.
Structured elaboration
- Detection. A row-count match tells you nothing about this class of bug; you need a comparison that actually checks the specific field the constraint mismatch affects. For a small enough table, a full scan comparing every record's relevant field between legacy and new is feasible; for a larger one, sampling (checking a statistically meaningful, randomly selected subset regularly) trades completeness for cost, with the understanding that sampling can miss a rare-but-real divergence, so it should be paired with targeted checks wherever you have reason to suspect a specific failure mode.
- Root-cause the specific mismatch. In this case, the legacy system allowed nullable customer IDs and the new service requires non-null, so every legacy record with a null customer ID either failed to migrate, migrated with a placeholder value, or (worse) migrated silently incorrect depending on how the migration code happened to handle the null. Understanding exactly which of these happened determines what "repair" even means for each affected record.
- Automated, idempotent repair. Write a repair script that's safe to run more than once: it should check the current state of a record before "fixing" it, and skip records that are already correct, rather than blindly reapplying a transformation that would be wrong if run twice. For records with a genuinely missing customer ID, the repair might mean backfilling from another data source, flagging for manual review, or applying an agreed placeholder policy, a decision that needs a real answer, not a technical default chosen because it's convenient.
- Prevent recurrence. Fix the migration path itself (add validation or a default-handling rule for the null case) so new records don't keep hitting the same problem, and add the specific field check to your ongoing reconciliation process so a similar mismatch is caught quickly next time rather than accumulating for weeks before anyone notices.
Worked example
def find_and_repair_null_customer_id_divergence(legacy_conn, new_conn, sample_size=None, dry_run=True):
# Detect orders where legacy allowed a null customer_id and the new
# service's non-null constraint means the migrated record is missing,
# has a placeholder, or is otherwise wrong. Idempotent: re-running finds
# only records still in a bad state.
query = "SELECT order_id, customer_id FROM legacy_orders WHERE customer_id IS NULL"
if sample_size:
query += f" ORDER BY RANDOM() LIMIT {sample_size}"
legacy_null_orders = legacy_conn.execute(query).fetchall()
repaired, needs_manual_review = [], []
for order_id, _ in legacy_null_orders:
new_record = new_conn.execute(
"SELECT order_id, customer_id FROM orders WHERE order_id = ?", (order_id,)
).fetchone()
if new_record is None:
# never migrated at all: this is a missing-record gap, not a
# constraint violation per se; flag separately from bad-value cases
needs_manual_review.append((order_id, "missing_entirely"))
continue
if new_record[1] == "UNKNOWN_CUSTOMER_PLACEHOLDER":
# already carries the agreed placeholder: nothing to do, this
# branch is what makes re-running the script safe
continue
if new_record[1] is None:
needs_manual_review.append((order_id, "unexpected_null_in_new_system"))
continue
# any other case: customer_id is set to something real, so this
# record is not actually part of the divergence; leave it alone
continue
if not dry_run:
for order_id, reason in needs_manual_review:
if reason == "missing_entirely":
new_conn.execute(
"INSERT INTO orders (order_id, customer_id) VALUES (?, ?)",
(order_id, "UNKNOWN_CUSTOMER_PLACEHOLDER"),
)
repaired.append(order_id)
return {"checked": len(legacy_null_orders), "repaired": repaired, "needs_manual_review": needs_manual_review}
Run against a small in-memory fixture: two legacy orders with null customer_id, one missing entirely from the new system and one present with an unexpected null, plus one legacy order with a real customer_id that should be left untouched. Running the function with dry_run=True first reports one missing_entirely and one unexpected_null_in_new_system for manual review, and reports the third order as unaffected, which is the expected outcome; running it again with dry_run=False inserts the placeholder for the missing record, and running it a third time finds nothing left to do for that record, confirming the idempotency the design set out to guarantee.
Trade-offs and pitfalls
The pitfall this function is written to specifically avoid is a repair script that reapplies a fix blindly: if the placeholder-insert logic didn't check for the placeholder already being present, running the script twice would either error on a duplicate key or, worse, silently overwrite a value a human had since corrected manually. The trade-off in detection is sampling versus full scan: sampling is far cheaper at scale but can miss the specific instance of a rare divergence, which is why pairing sampling with targeted checks for known failure modes (like this specific nullable-versus-non-null mismatch) gives better coverage than sampling alone.
You inherit a system with almost no documentation, dependencies nobody wrote down, and the one engineer who understood it just left. What do you actually do in the first weeks, and how do you avoid either freezing all feature work or making the risk worse?
Sample Answer
Direct answer
The first weeks are about reducing risk cheaply, not about rewriting anything: map what the system actually does and who depends on it, put a safety net under it (monitoring and tests) before touching the code, and only then start making small, reversible changes. The goal by day 90 is not "the system is modernized," it is "the system is no longer a black box, and the org has evidence about what's safe to touch," while feature work continues in parallel rather than freezing entirely.
Structured elaboration
A workable structure:
- Days 1 to 30, discovery. Build a dependency map using a combination of static analysis (what does the code call), dynamic tracing and log correlation (what actually happens in production, which is often different from what the code suggests), and conversations with anyone who has touched the system, since undocumented tribal knowledge is itself a source you have to capture before it walks out the door. This is also when you look for lightweight, low-production-impact ways to instrument the system if it has no observability at all.
- Days 30 to 60, safety net. Add monitoring and alerting so you would actually notice if something broke, and add characterization tests, tests that pin down the system's current behavior (correct or not) so a later change that alters that behavior gets caught immediately, rather than tests that assert what the behavior should be, which requires understanding the system better than you do yet.
- Days 60 to 90, first small changes. Make a handful of low-risk, reversible improvements, fixing the most painful operational issue, extracting the most clearly separable piece, and use these as a way to validate that your dependency map and safety net actually work, before committing to anything bigger.
Throughout, the discovery approach itself matters: static analysis alone misses runtime-only dependencies (a job triggered by a cron entry nobody documented, a hidden call made only under a rare condition), so combining it with dynamic tracing, network traffic capture, and log correlation catches what static analysis alone would miss, at low production impact since these are observational techniques, not changes to the system itself.
To avoid freezing feature work: communicate explicitly that discovery and safety-net work is happening in parallel with, not instead of, feature delivery, and pick the first few changes specifically because they are small enough not to threaten the delivery timeline while still proving the approach works.
Worked example
An engineer inherits a legacy payment-reconciliation service: no documentation, three flaky integration tests, and the one person who understood it left six months ago.
- Week 1 to 2: they run static analysis to find every internal call path, and separately turn on request logging (a low-impact, purely observational change) to see what actually gets called in production. The two do not fully agree: static analysis misses a nightly batch job triggered by an external cron system nobody had documented, which the logs reveal because it shows up as unexplained traffic at 2am.
- Week 3 to 6: they build monitoring on the reconciliation service's key outputs (does the daily reconciliation total match expectations) so a regression would actually be visible, and write characterization tests around the three most business-critical code paths, capturing current behavior rather than guessing at intended behavior.
- Week 7 to 12: with the safety net in place, they fix the most painful operational issue (a memory leak that forces a weekly manual restart) as the first real change, verify the characterization tests and monitoring both catch the change as expected (a sanity check that the safety net itself works), and report back to stakeholders with an actual map of the system's dependencies and risk areas, rather than a vague "it's better now."
Trade-offs and pitfalls
The tempting shortcut is to skip straight to fixing the most obviously bad code, but without a dependency map and a safety net first, you cannot tell whether a "fix" broke something you didn't know depended on the old behavior, which is exactly how well-intentioned early changes to an undocumented system make things worse rather than better. The other trap is treating discovery as a one-time exercise rather than an ongoing habit; legacy systems that have been running for years often have dependencies that only surface under conditions (end of quarter, a specific customer's data shape) you will not see in the first 90 days no matter how thorough you are.
Walk through how you'd run a staged, dual-run migration where the old and new systems operate side by side for a while. What has to be true before you cut over the last piece of traffic?
Sample Answer
Direct answer
A staged, dual-run migration keeps the old and new systems live at the same time, moving traffic (or workloads) over gradually while continuously checking that the new side agrees with the old one, so you never bet everything on a single cutover moment. What has to be true before the last piece of traffic moves: the new system has matched the old one's behavior consistently, for long enough, across enough real traffic, that the remaining risk is genuinely small, not just "it's worked so far."
Structured elaboration
A concrete staged approach:
- Stand up the new system alongside the old one, with both able to receive traffic, but only the old one authoritative at the start.
- Shadow or dual-run a slice of traffic: send a copy of real requests to the new system without letting its response count, and compare its output against the old system's real response. This validates behavior with zero user-facing risk, since nothing depends on the new system's answer yet.
- Move real traffic incrementally, starting with a small, low-risk percentage or cohort, watching error rates, latency, and any business-correctness metrics you can measure automatically, before increasing.
- Reconciliation checks throughout, not just at each stage boundary: continuously compare outputs (or data state) between the two systems so drift is caught early rather than accumulating silently.
- Cutover gating: define explicit criteria for moving to the next stage, error rate under a threshold, reconciliation clean for a minimum window, no open incidents tied to the new system, rather than a subjective "seems fine" call.
For workloads with hard data-consistency requirements (a payments ledger, an inventory system, anything a customer-facing application depends on), the reconciliation step needs to specifically check data consistency, not just functional correctness, since the two systems could return the same answer to a query while their underlying data has quietly diverged.
A permanent variant of this pattern shows up whenever a piece of the old system is deliberately kept running for good, rather than the coexistence period being purely transitional. That's a different commitment than a staged cutover, and it needs a different kind of discipline: a stable, explicit integration boundary between the old and new pieces (an anti-corruption layer, not an ad hoc set of point-to-point calls that grows over time), clear, permanent ownership assigned to whoever maintains the piece that stays, so it doesn't quietly rot once attention moves to the new system, and a periodic, scheduled re-evaluation of whether "stays forever" is still the right call, since the reasons a capability was kept back (cost, risk, a hard dependency) can change well after the original migration project has wrapped up and everyone has moved on.
Worked example
Migrating a customer-facing web application from a legacy monolith to a new service-based platform in stages, preserving data consistency:
- Stage 0: the new version is deployed and can serve traffic, but all real traffic still goes to the old system. A shadow pipeline sends a copy of production requests to the new version and logs differences without affecting real users.
- Stage 1: shadow comparison runs clean for two weeks (response equivalence above 99.9%, with every discrepancy investigated and either fixed or explained). The team starts routing 5% of real traffic to the new version, chosen as a specific low-risk customer segment, with an instant rollback flag.
- Stage 2 through 4: traffic ramps 5% to 25% to 100% over several weeks, gated at each stage by error rate staying flat and a data-consistency check (comparing a sample of database state between the old and new systems) staying clean.
- Cutover complete: once at 100% for a defined period with no reconciliation issues, the old system is marked as the fallback rather than primary, and eventually decommissioned following the same discipline as any other legacy retirement.
Trade-offs and pitfalls
The main trade-off is time against certainty: a staged rollout with real gating criteria takes meaningfully longer than a single cutover, but it converts "we hope this works" into "we have evidence this works," which is exactly the trade a business accepting real downtime risk is making when it agrees to this approach. The pitfall to watch for is gating criteria that are too loose to catch a real problem (an error-rate threshold set so high that a meaningful regression still passes) or reconciliation checks that only validate the easy cases, functional correctness is necessary but not sufficient if the underlying data can still silently diverge.
Unlock Full Question Bank
Get access to all 36 Legacy Modernization and Architecture Evolution interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.