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.
What is the anti-corruption layer pattern, and what job is it actually doing when you put one between a legacy system and a new one? Walk through a concrete example of translating legacy data into a new service's model.
Sample Answer
Direct answer
An anti-corruption layer (ACL) is a translation boundary you deliberately put between a legacy system and a new one so that the new system's domain model never has to bend to accommodate the legacy system's quirks. It is not just an adapter that converts data formats; its job is to protect the new model's integrity by absorbing all the legacy system's inconsistencies, missing fields, and outdated assumptions on the legacy side of the boundary, so nothing about the old system's design leaks into the new one.
Structured elaboration
Concretely, an ACL is responsible for:
- Translation: converting the legacy system's data shapes, field names, and units into the new system's domain model, not the other way around.
- Mapping semantic gaps: the legacy system might represent a concept the new system does not have an exact equivalent for (a status enum with legacy-only values, a field that means two different things depending on another field). The ACL is where you decide how those map, once, in one place, instead of every consumer inventing its own interpretation.
- Isolation: consumers on the new side never call the legacy system directly or see its raw shapes. If the legacy system changes (or if you eventually replace it), only the ACL has to change.
The benefit is that your new services get to have a clean domain model that reflects how the business actually works today, not how a fifteen-year-old system happened to represent it. The cost is that the ACL itself becomes a piece of infrastructure someone has to own, test, and keep in sync as both sides evolve, and a badly maintained ACL can become exactly the kind of tangled legacy code it was meant to prevent.
Worked example
Say a legacy order system represents order status as an integer code (0, 1, 2, 9) where 9 means "cancelled" but also gets reused for "refunded" depending on an unrelated flag elsewhere in the record, a real and common kind of legacy inconsistency. A new order service wants a clean OrderStatus enum: PENDING, CONFIRMED, SHIPPED, CANCELLED, REFUNDED.
The ACL sits at the boundary and does the translation:
def translate_legacy_status(legacy_code: int, refund_flag: bool) -> str:
if legacy_code == 9 and refund_flag:
return "REFUNDED"
mapping = {0: "PENDING", 1: "CONFIRMED", 2: "SHIPPED", 9: "CANCELLED"}
return mapping[legacy_code]
Every consumer on the new side calls the ACL and gets back a clean OrderStatus, never the raw integer code or the refund flag. If the legacy system later adds a sixth status code, only this one function needs to change.
To validate this kind of adapter, the concrete test strategy is a contract test against a fixture of known legacy inputs paired with their expected new-model outputs, covering every legacy value (including the ambiguous ones like the reused code 9) and every combination the ACL has to disambiguate, not just the happy path. That test suite is what tells you the ACL is behaving correctly before any real traffic depends on it, and it is what catches the translation silently breaking if someone touches the mapping later.
Trade-offs and pitfalls
The main pitfall is letting the ACL grow into a second copy of legacy logic instead of a thin translation boundary: if it starts encoding business rules of its own rather than just mapping shapes, you have created a new piece of legacy code, not protected against the old one. The other common mistake is skipping the ACL for "just this one caller" because it seems faster, which reliably ends with the legacy system's quirks leaking into the new domain model through that one exception, and everyone downstream having to account for it.
You need to move a legacy authentication system, whether that means bridging it to a modern identity provider, extracting it into its own service, or eventually retiring it, without ever locking a real user out. Walk through how you would sequence that migration and what would make you halt it.
Sample Answer
Direct answer
Authentication is the single highest-blast-radius thing you will ever migrate, because getting it wrong does not just break a feature, it locks real people out of everything at once. The sequencing that keeps that from happening is: build the new path behind a feature flag while the old one keeps serving all traffic, validate the new path against a copy of real traffic before it serves anyone, cut over gradually with an instant rollback path, and only decommission the old system once nothing depends on it and you've confirmed there are no forgotten integrations still calling it directly.
Structured elaboration
Whether the end state is bridging to a modern identity provider, extracting auth into its own service, or eventually retiring the legacy system, the sequencing is the same shape:
- Dual-run, don't switch. Stand up the new auth path (a new identity provider, a new standalone service) alongside the old one. Route reads to both and compare results before routing writes.
- Solve token and session compatibility first. If you are bridging to a modern OIDC (OpenID Connect) provider, the hard problem is usually translating existing session state and tokens so users do not get silently logged out mid-migration; issuing brand-new tokens to everyone at once is what causes a lockout incident.
- Cut over by cohort, not all at once. Internal users first, then a small percentage of external users, watching login success rate and support ticket volume as the leading indicators, before expanding.
- Keep an instant kill switch. Auth is the one place where "roll back slowly" is not good enough; the flag that routes back to the old path needs to work in seconds, not require a deploy.
- Decommission last, deliberately. Once the new path is fully live, do a dependency audit before turning off the old auth service, because auth systems accumulate forgotten direct callers (internal admin tools, batch jobs, partner integrations) that never went through the front door.
What would make you halt the migration entirely: a spike in failed logins or session drops that you cannot immediately attribute and fix, evidence that the token-compatibility layer is silently issuing tokens with the wrong scope or identity, or discovering an undocumented caller that bypasses the new path in a way that would leave users unable to authenticate if you proceeded.
Worked example
A team migrating from a legacy LDAP-based SSO (LDAP: a decades-old protocol for looking up users and groups in a company directory) to a cloud-native OIDC provider:
- They stand up the OIDC provider and a token-translation shim that can validate both old session cookies and new OIDC tokens, so a user with an existing session is not forced to re-authenticate mid-transition.
- They route 1% of new logins through OIDC, comparing the resulting user identity and permission set against what LDAP would have returned for the same user, catching any mapping bugs (a legacy group membership that does not translate cleanly) before it affects more than a handful of people.
- They ramp cohort by cohort over several weeks, watching login failure rate as the primary rollback trigger.
- Once 100% of logins go through OIDC and session drop rate is flat, they audit for anything still calling the legacy LDAP service directly, find two internal admin tools that were, migrate those callers explicitly, and only then decommission LDAP, with a documented rollback window in case something surfaces post-decommission.
Trade-offs and pitfalls
The trade-off is speed against blast radius: auth migrations are one of the few places where moving deliberately slowly is the correct engineering decision, not caution for its own sake, because the cost of a mistake (locking out your whole user base) is asymmetric compared to almost anything else you could migrate. The most common pitfall is treating auth like any other service migration and cutting over faster than the token-and-session compatibility work actually supports, which produces exactly the kind of mass-logout incident the dual-run and cohort-based rollout are designed to prevent.
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.
What does 'ready to cut over' actually mean for a legacy replacement? Walk through how you'd decide go or no-go for a production cutover, and what a rollback trigger looks like in practice.
Sample Answer
Direct answer
Readiness before a production cutover means having evidence across people, process, and tooling that you can both execute the cutover safely and respond quickly if it goes wrong, not just that the new system passed its tests. The minimum sign-off criteria should require a named on-call owner, a tested rollback path (not just a documented one), and monitoring that would actually surface a problem within your acceptable detection window, all confirmed before the cutover window opens, not assumed.
Structured elaboration
A practical readiness checklist:
- People: who is on-call during and immediately after the cutover, do they have the context and authority to make a rollback call without escalating through several layers first, and is there a clear communication channel for status updates during the window.
- Process: is there a documented incident-response plan specific to this cutover (not the generic one), including who declares an incident, what the rollback trigger criteria are, and who needs to be notified externally (support teams, customers, partners) if things go wrong.
- Tools: is monitoring in place and validated (not just "we have Datadog," but "we've confirmed these specific dashboards would show this specific failure mode"), is CI/CD ready to ship a hotfix quickly if needed, and is backup/restore tested and current, not just configured.
The go/no-go sign-off itself should require explicit confirmation of each of these from the relevant owner, not a single person's overall gut check, since the person who built the new system is the least likely to notice a gap in the operational readiness around it, having been focused on the system itself rather than the surrounding safety net.
For a planned cutover from a legacy system specifically, the runbook needs additional content beyond a routine deployment: preparation steps unique to this being a one-time (or rare) event rather than routine (verifying the legacy system's final state, confirming no in-flight transactions will be lost mid-cutover), clearly assigned roles and responsibilities for the cutover window itself, explicit rollback criteria (what specific signal triggers falling back to the legacy system), smoke tests to run immediately after cutover before declaring success, and monitoring checkpoints for the hours and days following, since some classes of problem (a slow data leak, a rare edge case) only surface well after the cutover moment itself.
Worked example
A team preparing to cut over from a legacy billing system to a new platform:
- People: two engineers (not one) are scheduled on-call for the cutover window and the following 48 hours, with the on-call lead having pre-approved authority to trigger rollback without needing sign-off from a manager first, since waiting for that approval during an active incident costs exactly the time that matters most.
- Process: the runbook explicitly lists rollback criteria (error rate above a defined threshold for more than five minutes, or any detected billing discrepancy at all, given zero tolerance for revenue leakage) and a communication plan naming who tells customer support what to say if customers are affected.
- Tools: monitoring dashboards were validated in a rehearsal by deliberately injecting a known failure into the staging environment and confirming the dashboards actually flagged it within the target detection window, not just assumed to work because the metrics exist.
- Smoke tests: immediately post-cutover, an automated suite runs a set of real billing scenarios against the new platform and compares results to expected values before the team declares the cutover successful and stands down from heightened monitoring.
- Sign-off: the go/no-go meeting requires each of these four owners to explicitly confirm readiness; the cutover is delayed a day when the monitoring rehearsal reveals a dashboard gap, rather than proceeding on the assumption it would probably be fine.
Trade-offs and pitfalls
The trade-off is the discipline of a rehearsed, explicit checklist against the temptation to treat readiness as implicit once the code is done and tested; for a cutover from a legacy system, which usually can't be easily repeated if it goes wrong, that discipline is what separates a well-executed migration from an incident. The pitfall to watch for is a monitoring setup that exists but has never been validated against a real failure scenario, since "we have dashboards" and "our dashboards would actually catch this specific failure" are different claims, and only rehearsal proves the second one.
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.
Unlock Full Question Bank
Get access to all 10 Legacy Modernization and Architecture Evolution interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.