Modernization · 9 min read · Oct 12, 2026
The ERP stays as it is: building on a decades-old system through an anti-corruption layer
How to build new software on an old ERP without modifying it, using an anti-corruption layer: what goes inside, how to handle writes, when to skip it.
You do not have to change an old ERP to build modern software on top of it, and in most cases you should not try. What you need is one deliberately designed boundary: facing the ERP, it uses the ERP's vocabulary, and facing your new applications, it uses the terms your business works in today. That boundary is called an anti-corruption layer, and the quality of its design decides whether your new systems stay clean or slowly turn into extensions of the old one.
The problem it solves
An ERP that has run a company for twenty years carries twenty years of decisions in its data model. Field names are eight characters long. A column called CUSTTYPE holds A, B, 7 and X, and only two people know that 7 means "tax exempt, do not ship on Fridays". Dates are stored as integers. Deleted records are not deleted, they have a D in a status column. Quantities have implied decimals. A customer with three shipping addresses exists as three customer records with a naming convention.
None of this is a defect in the sense that matters to the business. The ERP closes the books every month. But when a new application reads those tables directly, every one of those conventions leaks into it. Soon the new order portal has if custType == '7' scattered through its code, the mobile app has its own copy of the same rule (slightly different), and the reporting service has a third. You now have several systems that all depend on undocumented legacy quirks, and changing or replacing the ERP has become harder than it was before you started.
The term comes from Eric Evans's work on domain-driven design. The idea is simple: when two systems have different models of the world, do not let one model seep into the other. Put a translator between them, and make the translator the only place that knows both.
What the layer is, concretely
Forget the name for a moment. In practice it is a small service (or a module with a strict interface) with two faces.
The inward face is an API or set of events designed around how your business talks today. It exposes Customer, Order, Shipment, Invoice, with clear field names, real types, ISO dates, explicit currencies and documented states. It knows nothing about table names.
The outward face talks to the ERP in whatever way the ERP allows: SQL against its database or a replica, a vendor API if one exists, stored procedure calls, flat file import and export, a message queue, sometimes a terminal session.
Between the two sits the translation. New applications only ever see the inward face. If the ERP is upgraded, re-platformed or eventually replaced, the outward face and the translation change. The applications do not.
What belongs inside, and what does not
The layer is useful only if it has a narrow job. A practical split:
| Belongs in the layer | Belongs somewhere else |
|---|---|
| Mapping legacy fields and codes to clean names and enumerations | Pricing rules, approval rules, any new business logic |
| Type and unit conversion (integer dates, implied decimals, padded strings, code pages) | Workflow and orchestration across several systems |
| Reassembling one business entity from many legacy rows or tables | User interface concerns and presentation formatting |
| Splitting one clean entity into the records the ERP expects | Reporting and analytics queries |
| Protocol handling: connection pooling, retries, timeouts, batching | New data that the ERP has no place for |
| Translating ERP errors into meaningful domain errors | Features of the new applications |
| Protecting the ERP: rate limits, concurrency caps, batch windows | |
| Logging every call that crosses the boundary |
The left column is all translation and protection. The moment a rule in the layer answers the question "what should the business do?" instead of "how does the ERP represent this?", it is in the wrong place.
A few details that are easy to miss:
- Identity mapping. The ERP's keys are often composite, reused or meaningful (a customer number with the branch encoded in it). The layer should issue or map stable identifiers so that new systems never parse a legacy key.
- Code tables as data. Keep the mapping of
7toTAX_EXEMPTin a versioned table or config, not in anifstatement. Somebody will add a new code in the ERP without telling you. The layer should detect an unknown code, log it loudly and fail in a defined way, not pass it through. - The undocumented rules. Spend time with the people who use the ERP daily. The rule that an order with a blank ship date means "hold" is not in any manual. Capture each one as a test against the layer.
Reading is the easy half
Reads have a comfortable property: if you get them wrong, you can fix the code and read again.
The main design question for reads is where they hit. Three options, in rough order of preference:
- A replica or replicated copy. Queries from new applications never touch the production ERP database. You accept a delay, which you should measure and publish ("inventory is at most five minutes old").
- A cache or read model owned by the layer. The layer pulls changes on a schedule or through change data capture and serves reads from its own store. This also lets you shape the data for the new applications.
- Direct reads against production. Sometimes unavoidable for data that must be current to the second, such as available credit at order time. Cap the concurrency, set timeouts and keep the queries simple, because a slow report from your new portal should never be the reason month-end close stalls.
Be explicit about freshness for each kind of data. Product descriptions can be a day old. Stock on hand usually cannot.
Writes are where the design is decided
Writing into an old ERP is a different kind of problem, for one reason: the ERP's own screens and programs enforce rules that the database does not.
When a clerk enters an order through the ERP, the application code checks credit, reserves stock, assigns a number from a sequence, updates three summary tables and queues a pick ticket. If your layer inserts a row straight into the order table, none of that happens. The row exists, the totals are wrong, and the warehouse never sees it. Depending on the vendor, direct writes may also put your support agreement at risk, which is a question for your contract and your vendor, not for a guess.
So the first rule of writes is to go through whatever path runs the ERP's own logic. In order of preference:
- A supported API or business object interface, if the ERP has one.
- A supported import mechanism: a staging table, an interface file, an EDI-style inbound process that the ERP validates and posts itself.
- Stored procedures the vendor or your team already maintains for that transaction.
- Driving the user interface with automation, as a last resort, wrapped inside the layer so that nothing else knows it is happening.
- Direct table writes, only for tables you fully understand, with the ERP owner's agreement, and almost never for transactional data.
The second rule is that every write must be safe to retry. Networks fail between "sent" and "confirmed". The layer needs an idempotency key per business operation, stored on its side, so that a retried order submission is recognized and not posted twice.
The third rule is to accept that many legacy write paths are asynchronous. A file dropped at 2:10 pm is picked up at 2:15 and posted, or rejected, a few minutes later. Design the inward API to reflect that honestly: the caller gets "accepted, pending" with a reference, and learns the outcome through a status query or an event. Pretending the write is synchronous leads to timeouts and to users who click submit three times.
A reliable shape for this is an outbox: the new application records the intent in its own database in the same transaction as its own changes, a worker in the layer delivers it to the ERP, and the result (posted, rejected with reason) is written back. Add a daily reconciliation job that compares what the new systems believe was sent with what the ERP actually holds. That job will earn its keep the first week.
Picture a manufacturer whose customer portal lets buyers place orders. The portal calls POST /orders on the layer. The layer validates the shape, maps products and units to ERP item codes, writes a record to the ERP's order import staging table and returns a pending reference. The ERP's nightly and hourly import jobs apply credit and stock rules. Rejections come back as status rows, which the layer translates into "credit hold" or "item discontinued" and passes to the portal. The ERP's code was never touched, and every rule it enforces still applies.
Keeping the layer thin
Boundary layers have a natural tendency to grow, because they are the one place everything passes through and adding "one small rule" there is always the quickest fix. A year later the layer is a second ERP with no documentation.
Habits that prevent it:
- One reason to change. The layer changes when the ERP's representation changes or when a new entity needs translating. A change request that begins with "the business wants" belongs in a domain service.
- No orchestration. If an operation involves the ERP, the CRM and the shipping carrier, a separate service coordinates them. The layer only handles its own system.
- Minimal state. Idempotency keys, identity maps, an outbox and possibly a read cache. No business records of its own.
- Contract tests against the real thing. Keep a test suite that runs against a non-production copy of the ERP, with recorded samples of ugly real data. ERP patches and custom report changes break integrations silently, and this suite is how you find out before users do.
- A named owner. Someone is responsible for the mapping tables and for saying no.
- Publish only what is used. Expose the five entities the new applications need, not all four hundred tables. Each exposed field is a promise you must keep.
When you can skip it
A translation layer is an extra component to build, deploy and monitor. Sometimes it is not worth having.
- One small, short-lived integration. A single nightly export to one consumer does not need an architecture. A well-commented script and a mapping file are fine.
- The ERP's model is already close to yours. If the system is reasonably modern, has a documented API and uses concepts that match how you work, a thin client library is enough.
- Analytics only. If all you need is reporting, load the data into a warehouse and do the cleanup in transformation models. That is a translation layer of a kind, built with different tools.
- Replacement is imminent and committed. If the ERP is being switched off in six months with budget approved, invest in the migration instead.
- The vendor's integration layer already does the job. If a supported integration product gives you a clean, stable contract, use it and keep your own code on the far side of it.
The warning sign that you do need one: a second new application is about to integrate with the ERP, and the plan is to copy the SQL from the first.
How many of your systems know what your ERP's column names mean?
If the answer is more than one, the ERP's quirks are already spreading. It is worth mapping where, and deciding what a clean boundary would look like before the next application gets built on top. Trivo can do that mapping with your team and hand you a written design you own.
Share this article
Keep reading
Data · 9 min read
Transform before or after loading? A practical ETL versus ELT decision for mid-sized teams
ETL or ELT is a question of where transformation runs and who pays for it. A practical way for a mid-sized team to choose, with the costs most plans miss.
Oct 12, 2026 Read
AI · 9 min read
Bots on the screen or calls to the API: where RPA earns its place and where it becomes debt
RPA and API integration solve the same problem at different layers. How to choose, what screen automation really costs, and how to run it safely if you must.
Oct 12, 2026 Read
Modernization · 9 min read
Replace the legacy system in slices: a working guide to the strangler fig pattern
How to retire a legacy system piece by piece while the business keeps running: the facade, the first slice, parallel runs, and when the pattern fails.
Oct 12, 2026 Read