Anti-corruption layers and the strangler fig: adding AI to a client's legacy system without breaking it
You don't have to rewrite the client's old system from scratch. The new AI layer also shouldn't pick up the old system's messy conventions.

In brief
- An anti-corruption layer translates between two systems that read the same data differently. The new AI layer keeps its own design, and the legacy system needs no changes.
- The strangler fig pattern puts a façade in front of the old system to intercept requests and move functions to a new service one by one. Callers never know a migration is under way.
- Both layers add latency and can fail, so you need retries, circuit breakers, correlation IDs and a plan for removing them.
In your second week at a client, you open the database of the warranty management system they have run for more than ten years. The TRANG_THAI (status) column holds only three letters: C, D and H. Dates are stored as strings.
The MO_TA (description) column mixes the customer’s account of the fault with staff’s internal notes. Meanwhile, the client wants an AI agent classifying warranty claims this quarter.
Most engineers fall into one of two traps. The first is to let the agent read TRANG_THAI = 'D' directly. After that, every prompt, every eval and every schema has to “know” what D means. The second is to propose rewriting the whole system, a project the client will never approve.
Two patterns get you between those traps: the anti-corruption layer (ACL) and the strangler fig. If FDE work sends you to a client with a legacy system nobody dares touch, learn both patterns before the first architecture meeting. You will then have a plan the client is willing to sign off.
Why does a legacy system “infect” new code?
Microsoft’s Azure architecture guidance names the problem. When a new system has to talk to a legacy one, it is forced to adopt at least part of the legacy system’s API or semantics. Nobody decides to do this. It creeps in, one if status == 'D' at a time.
The ACL stops that from happening. Eric Evans first described the pattern in Domain-Driven Design. It is a facade or adapter layer between two systems that read data differently, and its job is to translate each side’s requests into the other side’s language.
According to Microsoft, this lets one system stay unchanged without compromising the design and technology of the other.
In an AI project, “the other system” is what you build: the schema the agent takes as input, the eval suite, the tool definitions. You want them to speak the language of the business, such as pending, in_progress and closed, not the language of a database designed many years ago.
How do you write an ACL for the warranty system?
Back to the opening example. After asking the customer care team, you learn that C means “awaiting processing”, D means “in progress” and H means “completed”. Internal notes always start with [NB]. This is the core of the translation layer:
from dataclasses import dataclass
from datetime import date, datetime
STATUS_MAP = {"C": "pending", "D": "in_progress", "H": "closed"}
@dataclass
class WarrantyClaim:
claim_id: str
status: str
created_on: date
customer_text: str
internal_notes: list[str]
class LegacyFormatError(ValueError):
pass
def to_domain(row: dict) -> WarrantyClaim:
code = (row.get("TRANG_THAI") or "").strip()
if code not in STATUS_MAP:
raise LegacyFormatError(f"Unknown status code: {code!r} (id={row.get('MA_BH')})")
lines = (row.get("MO_TA") or "").splitlines()
return WarrantyClaim(
claim_id=row["MA_BH"],
status=STATUS_MAP[code],
created_on=datetime.strptime(row["NGAY_TAO"], "%d/%m/%Y").date(),
customer_text="\n".join(l for l in lines if not l.startswith("[NB]")),
internal_notes=[l[4:].strip() for l in lines if l.startswith("[NB]")],
)
Three things are worth noting. First, the agent only ever sees WarrantyClaim. It does not know C, D and H exist.
Second, an unknown or empty status code makes the function fail at the boundary instead of slipping into a prompt. This follows Microsoft’s advice to validate input at the boundary. (The error message, in Vietnamese, reads “Unknown status code”.)
Third, internal notes are split out, so you can decide not to send them to the model.
The function contains no business logic. It does not decide which claims get priority or which go to a technician. Microsoft says plainly that business rules and orchestration do not belong in this layer.
What if the AI has to replace an old function outright?
The ACL lets the AI layer read legacy data. Clients usually want more. Fault classification is currently done on a manual data-entry screen, and they want AI to take it over. That is when you use the strangler fig.
Martin Fowler argues that replacing a large system in one go usually fails, and the approach he and his colleagues chose is gradual modernisation. Replacing a small piece at a time keeps the risk of each release small. Users also get new features without waiting for a replacement project that runs for years.
According to Microsoft, the strangler fig works through a façade (proxy) that intercepts requests to the legacy system and sends each one either to the old system or to the new service.
Callers don’t know a migration is happening. AWS makes the same point: because callers need no changes when their calls are quietly redirected, migration risk and the chance of disrupting the business both go down.
Here is how the façade might work in the warranty example. A POST /claims/classify request that used to go straight into the legacy system now passes through the façade.
The façade reads a feature flag. For a group of pilot branches, it sends the request to the AI service, with the data passing through to_domain first. All other branches run as before.
If something goes wrong, you turn the flag off. Nothing needs redeploying.
Microsoft recommends running an ACL throughout a strangler fig migration to manage dependencies between the old and new systems. Without that adapter layer, cross-dependencies can break components or force the new system to follow old conventions. The two patterns work together: the façade decides where a request goes, and the ACL translates it when it goes to the new side.
What should you ask before promising the client anything?
The strangler fig has prerequisites. Microsoft says it is a poor fit if you cannot intercept requests or cannot get at the legacy source code. To switch off a migrated function and redirect internal calls, you have to be able to change the old code.
So in the first week, ask the client two questions: where do requests enter the system, and who holds the source code?
If the answer is “all the logic is in stored procedures and nobody dares touch them”, you still have options. Microsoft suggests using triggers or change data capture so the database itself sends events to the new service, with no change to application code.
For example, each time a new row lands in the warranty table, CDC pushes an event onto a queue. The AI service picks up the event through the ACL and writes its suggested result to a separate table. The legacy system keeps running untouched, and you get real data to prove the new service before asking for permission to redirect requests.
The most common mistakes
The first mistake is forgetting that every new layer has a cost. Microsoft notes that an ACL adds latency to calls between the two systems and adds one more service to run. If the agent already spends a few seconds on each model call, measure how much the translation layer adds before the client notices.
The second is letting the façade become a bottleneck. Microsoft warns that the façade must not become a single point of failure or a performance bottleneck. AWS points out that the ACL can also fail, so you need retries and circuit breakers. When the AI service is slow, the circuit breaker should send requests back to the legacy system rather than leave users waiting.
The third is having no visibility. When a warranty claim is misclassified, you need to trace it through the façade, the ACL and the model. So every request should carry a correlation ID, and logs should be structured, from day one.
The last is treating a temporary layer as permanent. AWS advises that if the ACL is only a stopgap, you should record it as technical debt and remove it once every caller has migrated.
If you won’t be on the project by then, write the conditions for removing the ACL into the handover documents so the client’s team knows when they can delete it.
How do you show this skill on a CV?
When you write a CV for FDE roles, describe your legacy-system work in this order: what the old system was, where you intercepted requests, which function moved first, and how you could roll back at once if something broke.
A line such as “gradually moved classification to a new service behind a feature-flagged façade, with no changes to callers” is far more convincing than “experienced with microservices”.
If you haven’t done this yet, set yourself an exercise: take an old CRUD application, put a proxy in front of it, and move one endpoint to a new service with an ACL alongside.
The client doesn’t need to hear that their system is old. They need a plan that lets AI come in one piece at a time and be pulled out at any moment.
Was this article useful?
Thanks for the feedback!