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 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.
A legacy system speaks a protocol your new services do not (SOAP, a proprietary mainframe queue, or similar), and you need new consumers to work against it without waiting for the legacy side to change. Design the adapter layer that sits between them and describe how you would keep it from becoming a second system to maintain forever.
Sample Answer
Direct answer
Put a dedicated adapter service (not a shared library scattered across callers) between the legacy protocol and your new consumers, responsible for protocol translation, authentication bridging, and error-shape normalization, and treat it as a temporary, shrinking piece of infrastructure with an explicit plan to retire it, not a permanent integration point. The design decisions that matter most are where it lives (sidecar versus a shared gateway), how it handles the legacy system's throughput and latency ceiling, and how you keep it from quietly becoming a second system nobody wants to touch.
Structured elaboration
The core responsibilities of the adapter:
- Protocol translation: converting between the legacy wire format (SOAP, a proprietary queue protocol, fixed-width records) and whatever your new services speak (REST, gRPC, JSON).
- Schema and semantic mapping: legacy field names, units, and enums translated to the new domain model, the same discipline as an anti-corruption layer, because this adapter usually needs to be one.
- Authentication bridging: legacy systems often use older auth mechanisms (SAML, an older XML-based single sign-on standard; mutual certificates; static API keys); the adapter is where that gets exchanged for whatever your new services use, most commonly OAuth 2.0 tokens, with mTLS (mutual TLS) reserved for machine-to-machine cases rather than being an equally likely default.
- Backpressure and resilience: legacy systems frequently cannot handle the request volume a modern service mesh can generate. The adapter needs connection pooling, request batching, and its own rate limiting so it does not accidentally take down the legacy system it is protecting new consumers from.
On placement: a sidecar (deployed alongside each consuming service) minimizes added network hops and keeps the blast radius of a failure small, at the cost of deploying and versioning the adapter logic N times. A shared gateway centralizes the translation logic in one place, which is easier to evolve and monitor, at the cost of being a single point of failure and a potential throughput bottleneck if the legacy system's guaranteed-once processing requirements mean requests cannot simply be load balanced across replicas without care. For a small number of consumers with strict latency budgets, a sidecar is usually right; for many heterogeneous consumers, a shared gateway with careful capacity planning is usually right.
To avoid the adapter becoming permanent: track what fraction of the legacy system's capabilities still route through it, treat new special cases added to the adapter as a signal that a capability just got more entangled rather than less, and set an actual target date to revisit whether the adapter can start shrinking.
Worked example
A legacy mainframe payments system exposes a proprietary message-queue protocol with strict guaranteed-once processing semantics: the mainframe cannot handle duplicate submissions, and it cannot tell a modern REST client "I got that, don't worry." The adapter needs to:
- Accept REST requests from new services and assign each one an idempotency key before submitting to the mainframe queue, so a retried REST call does not become a duplicate mainframe transaction.
- Maintain a connection pool to the mainframe queue sized to its actual capacity, not the capacity of whatever load-balanced modern service is calling it, and queue or reject excess requests with a clear error rather than letting them silently pile up.
- Translate the mainframe's fixed-format response codes into REST status codes and structured error bodies the new services can actually branch on.
- Emit metrics on translation errors and latency added, since "how much is this adapter costing us" is exactly the number that later justifies (or delays) retiring it.
For the case where a third-party vendor keeps owning part of the flow (say, payment processing) for a bounded transition period, the same adapter pattern applies but with an explicit compatibility window: the adapter needs graceful-degradation fallback flows if the vendor's system is slow or down, SLA monitoring against the vendor's contracted latency, and a version-compatibility check so a vendor-side change does not silently break translation.
Trade-offs and pitfalls
The biggest pitfall is under-provisioning the adapter for the legacy system's real limits: a modern service tier can generate far more concurrent load than a mainframe queue was ever designed for, and the adapter's job is partly to be a deliberate throttle, not just a translator. The second is scope creep: once an adapter exists, it is tempting to route every new integration through it "since it's already there," which is exactly how a temporary migration aid becomes a permanent, poorly-owned piece of critical infrastructure that outlives the legacy system it was built to retire.
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.
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.
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 10 Legacy Modernization and Architecture Evolution interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.