Integrating a 20-year-old ERP without touching it
An anticorruption layer lets new systems talk to an untouchable ERP without inheriting its data model. What belongs in it, and how to keep it thin.
There is a category of system nobody is allowed to change. The ERP that has been running since 2005. The scheduling engine whose author left in 2013. The claims system that is certified, and recertifying it costs more than the project you are trying to do.
You still have to integrate with it. The question is how to do that without the new system inheriting thirty years of accumulated modeling decisions.
What an anticorruption layer actually is
The term comes from domain-driven design, and it is more literal than it sounds. The layer exists to stop one system's model from corrupting another's.
Concretely: a translation boundary that speaks the legacy system's language on one side and your domain's language on the other. Nothing on your side of the boundary knows that CUST_STAT_CD exists, that 9 means active, or that a customer with no rows in CUST_ADDR is a placeholder rather than a real customer.
What belongs inside it
The layer earns its keep by absorbing five specific kinds of ugliness. If it is not doing at least three of these, it is a pointless hop.
| Concern | Belongs in the layer | Example |
|---|---|---|
| Vocabulary | Yes | CUST_STAT_CD 9 becomes status: active |
| Shape | Yes | Six flat tables become one Customer with an address list |
| Missing semantics | Yes | A null end date means open-ended, not unknown |
| Access protocol | Yes | SOAP, a fixed-width file drop, or a direct read replica |
| Rate and batch limits | Yes | The ERP accepts 5 requests per second before it degrades |
| Business rules | No | Discount eligibility belongs to your domain, not the boundary |
| Caching policy for callers | No | Each consumer knows its own freshness requirement |
The last two rows are where these layers go wrong. Once business logic starts landing in the translation boundary, it stops being a boundary and becomes a second application that nobody owns.
Reads are easy, writes decide the design
Reading from an untouchable system is mostly a data problem. Writing to it is a consistency problem, and it drives the whole design.
Three options, in increasing order of what they cost you:
Write through the ERP's own interface. If it exposes an API or an import format, use it and accept its validation. Your layer translates and forwards. This is the only option that keeps the ERP as the single source of truth, which is almost always what you want.
Write to a queue the ERP drains. When the ERP can only ingest in batches, you accept eventual consistency and make it explicit. The new system must be able to show a pending state, and the user has to understand that "saved" is not "posted". This is a product decision, not just a technical one.
Write directly to its tables. Sometimes the only option. Treat it as a last resort with a written list of every constraint and trigger you are bypassing, because you are now responsible for invariants the application used to enforce.
Keeping it thin
Anticorruption layers grow. Every consumer that needs one more field, every edge case, every "while we are in there" adds a line. After two years the layer is bigger than the service it was protecting.
Three habits keep it small:
- One layer per legacy system, not per consumer. Multiple translation layers over the same ERP means multiple interpretations of the same field, and they will disagree.
- The layer has no database. The moment it stores state, it becomes a source of truth and stops being translation. Caching is fine; caching with its own write path is not.
- New fields require a named consumer. "We might need it" is how a 40-field translation grows into a 300-field one.
When you do not need one
If the legacy system already exposes a clean, well-modeled API, wrapping it adds a hop and a deployment for no benefit. Call it directly.
If you are actively replacing the legacy system rather than integrating with it, the layer may still be worth building, because it becomes the facade in a strangler-fig migration. But then it is a migration artifact with a retirement date, and it should be planned as one.
And if the "integration" is really screen automation because there is no interface at all, you are in different territory with different tradeoffs, covered in RPA versus API integration.
This is a large share of what we do on modernization work. If the system holds regulated data, the access rules in our security practices apply to the layer as much as to anything else.