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.
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.
You inherit a legacy system running on infrastructure (a mainframe, an unsupported platform, or similar) that the business cannot tolerate downtime or data loss from, with real compliance exposure if you get the migration wrong. How do you approach modernizing it?
Sample Answer
Direct answer
When the legacy system runs somewhere modern tooling barely reaches (a mainframe, an unsupported OS, proprietary hardware) and the business genuinely cannot absorb downtime or data loss, the approach is staged and conservative by design: rehost or wrap first to buy breathing room without touching the risky internals, then incrementally refactor or replace behind that wrapper, with retirement criteria and compliance sign-off treated as first-class deliverables, not an afterthought once the technical work is done.
Structured elaboration
A workable staged plan looks like:
- Rehost or API-wrap before you refactor. Get the system onto supportable infrastructure, or put a modern API in front of it, without changing its internal logic. This buys time and reduces operational risk (older hardware failing, staff who understand the system leaving) without touching the highest-risk code.
- Cost and risk analysis before committing to a target architecture. For a system with real compliance exposure, the honest options are usually rehost, wrap-and-strangle (wrap the legacy system behind a new API now, then incrementally re-implement and cut pieces over behind that wrapper later), or replace with a vendor product, and the choice should be driven by where the audit and revenue risk actually sits, not by which option is technically most interesting.
- Refactor incrementally behind the wrapper, treating each extracted piece with the same rigor as the strangler-fig capability extraction described above (the same wrap-and-strangle approach, now executed piece by piece): parallel-run it against the legacy path, verify identical output, then cut over.
- Define retirement criteria up front. For a compliance-sensitive system this usually means: a defined retention period for historical data in a queryable archive, a compliance sign-off checklist, and proof that every downstream consumer has migrated off the legacy interface, not just that the new system works.
- Build governance into the plan, not around it: who approves each cutover stage, what the audit trail requirements are for the migration itself (not just the resulting system), and how revenue-affecting changes get an extra review gate.
If the system is used globally, add explicit handling for data sovereignty (which regions' data can legally move where), multi-region active-active requirements if the system serves users with low-latency expectations across regions, and a per-region cutover sequence with its own compliance validation, since a single global cutover date is rarely realistic once regional regulatory approval is in the critical path.
On rehost-first ("forklift") specifically: virtualizing or rehosting the legacy application before refactoring reduces operational risk quickly, but it does not reduce the technical or compliance risk buried in the application logic itself, so it should be paired with parallel testing and a prioritized backlog of what still needs refactoring, not treated as the finish line.
Worked example
A team modernizing a mainframe billing system doing monthly batch settlements, with zero tolerance for revenue leakage:
- Phase 1 (rehost): they move the mainframe workload to an emulated environment on modern infrastructure, changing nothing about the COBOL logic, primarily to stop depending on aging hardware nobody can source parts for anymore.
- Phase 2 (API wrap): they build a service that exposes the settlement process through a modern API, so new reporting and reconciliation tools can integrate without touching the COBOL directly.
- Phase 3 (incremental refactor): they identify the least revenue-sensitive settlement rule (a fee calculation used by a small customer segment) and reimplement just that rule in a new service, running it in parallel against the legacy output for two full billing cycles before trusting it, because "the numbers matched in staging" is not sufficient evidence for a system with zero tolerance for revenue leakage.
- Retirement criteria: the legacy COBOL code for a given rule is only removed once its replacement has matched legacy output for a full compliance-relevant reporting period, and the audit function has signed off that the new system's controls satisfy the same requirements the old one did.
Trade-offs and pitfalls
The trade-off is speed versus provable correctness: the staged, parallel-run-everything approach is slow, but for a system where a mistake means real revenue leakage or a compliance failure, that slowness is the entire point, not an inefficiency to optimize away. The most damaging pitfall is treating the rehost step as the modernization itself and declaring victory once the hardware problem is solved, leaving the actual compliance and technical debt in the COBOL logic completely untouched.
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.
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 session and state handling out of a stateful legacy system and into something that can run as stateless services. What are the real options, and what does each one cost you?
Sample Answer
Direct answer
Moving session and state handling out of a stateful legacy system means choosing where that state lives once it's no longer just sitting in a single process's memory, and the real options (an external session store, stateless tokens, or continuing to pin users to specific instances) trade off differently on scaling, security, and latency, so the right choice usually depends on which of those three the system cares about most.
Structured elaboration
- Sticky sessions: keep routing a given user to the same server instance that holds their in-memory session, via a load balancer configuration. This is the least invasive change (session logic barely has to change) but it directly fights the goal of moving to stateless microservices, since it reintroduces a coupling between a user and a specific instance, which limits scaling flexibility (that instance can't be freely recycled or load-balanced away from) and makes failover harder (losing that instance loses the session).
- Centralized session store (for example, Redis): move session data out of application memory into a shared, external store that any instance can read from. This genuinely decouples the user from any specific instance, enabling real horizontal scaling and stateless application servers, at the cost of a network hop on every request that needs session data, and a new piece of infrastructure (the store itself) that needs its own availability and scaling story.
- JWTs (JSON Web Tokens) or other stateless tokens: encode the session data directly in a signed token the client holds and presents on each request, so the server needs no session storage at all. This scales the best (no shared state, no store to manage) and removes a network hop, but it introduces real trade-offs: tokens are harder to revoke immediately (a compromised token is valid until it expires unless you add a revocation mechanism, which reintroduces some server-side state anyway), and token size grows with how much session data you encode, which has real performance implications if overused.
- Token revocation strategies specifically matter for security: a short token expiry limits the blast radius of a compromised token but forces more frequent re-authentication; a server-side revocation list (a denylist of tokens invalidated before their natural expiry) restores immediate revocation at the cost of reintroducing a lookup on every request, partially undermining the stateless benefit that made tokens attractive in the first place.
The trade-offs, summarized: sticky sessions trade scalability for simplicity; a centralized store trades a network hop and new infrastructure for real statelessness at the application layer; tokens trade immediate revocability (unless you add it back) for the best scaling and latency characteristics.
Worked example
A team modernizing a stateful monolith that keeps user sessions in memory, migrating to stateless microservices:
- They rule out sticky sessions early: the whole point of the migration is elastic, independently-scalable microservices, and sticky sessions would undermine that goal directly for the sake of an easier migration.
- They choose a hybrid: session data itself (cart contents, in-progress form state) moves to a centralized Redis store, accessible from any service instance, while authentication state specifically uses a short-lived JWT (10-minute expiry) with a refresh-token mechanism, balancing the token's low-latency, no-lookup benefit for the common case (an authenticated request) against a short enough expiry to bound the blast radius of a compromised token.
- Session migration strategy: rather than forcing every active user to re-authenticate at cutover, the migration includes a compatibility step where the legacy in-memory session is looked up (via a bridging service) on a user's first request post-migration and transparently written into the new Redis store, so an active user experiences no visible disruption, an important detail for maintaining a seamless experience during the transition.
- Trade-off accepted explicitly: the team accepts the added Redis lookup latency (measured and confirmed to be within their latency budget) in exchange for real statelessness at the application layer, judging that trade-off better for their use case than pushing more into the JWT and accepting weaker revocation guarantees.
Trade-offs and pitfalls
The pitfall to watch for is choosing tokens purely for their scaling and latency benefits without thinking through revocation, and only discovering the gap when a real security incident requires revoking a compromised session immediately and the team realizes tokens are valid until they naturally expire. The broader lesson is that "stateless" is a spectrum, not a binary: a centralized store is still shared state, just moved out of application memory, and even a token-based approach usually needs some server-side state (a revocation list, a refresh-token store) once you account for real security requirements, so the honest comparison is about where the state lives and what it costs, not whether state exists at all.
Unlock Full Question Bank
Get access to all 31 Legacy Modernization and Architecture Evolution interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.