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 layerBelongs somewhere else
Mapping legacy fields and codes to clean names and enumerationsPricing 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 tablesUser interface concerns and presentation formatting
Splitting one clean entity into the records the ERP expectsReporting and analytics queries
Protocol handling: connection pooling, retries, timeouts, batchingNew data that the ERP has no place for
Translating ERP errors into meaningful domain errorsFeatures 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 7 to TAX_EXEMPT in a versioned table or config, not in an if statement. 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:

  1. 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").
  2. 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.
  3. 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:

  1. A supported API or business object interface, if the ERP has one.
  2. A supported import mechanism: a staging table, an interface file, an EDI-style inbound process that the ERP validates and posts itself.
  3. Stored procedures the vendor or your team already maintains for that transaction.
  4. Driving the user interface with automation, as a last resort, wrapped inside the layer so that nothing else knows it is happening.
  5. 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.

Book your Two-Week Discovery

Share this article

Keep reading