Migrating Legacy On-Premise ERPs to Cloud APIs Without Downtime
Learn how system integrators use the Strangler Fig pattern, dual writes, and declarative unified APIs to migrate enterprise ERPs to the cloud without breaking B2B SaaS connections.
Your enterprise customer just announced a 14-month migration from on-premise SAP ECC to S/4HANA Cloud. Or NetSuite from a heavily customized 2018-era account to a fresh OneWorld instance. Or JD Edwards to Oracle Fusion. Whatever the flavor, the message to your B2B SaaS product is the same: the integration you spent two quarters hardening is about to become a moving target, and any synchronization failure during cutover will be visible at the CFO's desk.
When enterprise customers migrate from legacy on-premise ERPs to modern cloud APIs, system integrators prevent downtime by decoupling the integration layer. They use the Strangler Fig pattern to incrementally route traffic, dual-write infrastructure to maintain state consistency, and a declarative Unified API to abstract the underlying protocol changes. If your B2B SaaS product relies on a direct, hardcoded connection to a legacy system, an upstream migration will break your application.
This guide is for the senior PMs and integration architects who own that risk. It covers how to keep your SaaS product wired into a customer's ERP estate while the underlying system shifts beneath it, detailing the architectural patterns required to survive an enterprise ERP migration, how to handle schema drift during the transition, and why abstracting the connection layer is the only way to maintain zero downtime.
The Enterprise ERP Migration Cliff
Short answer: when an enterprise customer migrates ERPs, your integration's reliability becomes their migration risk. Architect for parallel operation, not cutover, or you will be the line item that delays the project.
If you build B2B software that touches procurement, human resources, or accounting, your product lives and dies by its connection to the customer's Enterprise Resource Planning (ERP) system. For the last decade, connecting to these systems meant wrestling with on-premise servers, XML-heavy SOAP endpoints, and brittle VPN tunnels. Now, a massive shift is underway. Enterprises are actively decommissioning their on-premise monoliths in favor of modern, cloud-native SaaS ERPs.
The stakes for these migrations are incredibly high, and the costs are not abstract. ITIC's 2024 research shows the cost of hourly downtime continues to spike, with the average cost of a single hour now exceeding $300,000 for over 90% of mid-size and large enterprises. 44% of mid-sized and large enterprise respondents reported that a single hour of downtime can potentially cost their businesses over one million. For regulated verticals - Banking/Finance, Government, Healthcare, Manufacturing, Media & Communications, Retail, Transportation and Utilities - average hourly outage costs topped the $5 million mark.
When your SaaS product sits on the data path between a customer's CRM, billing engine, or HR system and their ERP, an integration outage during a migration window is functionally the same as an ERP outage. The customer's CFO does not care that the journal entries failed because your connector to NetSuite was looking for a sandbox subsidiary ID that no longer exists. They care that month-end close is now four days late.
As covered in our guide to enterprise integration strategy, moving upmarket means adopting an integration posture where the underlying ERP can change shape - new endpoints, new auth model, renamed custom fields, an entirely different protocol - without your application code changing at all. System integrators and enterprise engineering teams cannot afford to take systems offline for weeks to rewrite integration logic. They need a strategy that allows the legacy system and the cloud system to run concurrently, seamlessly shifting data flows without the core SaaS application ever knowing the difference.
Why the "Big Bang" Cutover Breaks B2B SaaS Integrations
The traditional approach to enterprise software upgrades is the "Big Bang" cutover. The IT team freezes the legacy on-premise server on Friday at 5:00 PM, runs a massive one-shot data migration script over the weekend, flips DNS, and prays the new cloud ERP works on Monday morning. It fails for the same reason monolith rewrites fail.
The big bang approach can be akin to chopping down the entire forest and starting anew - it's disruptive, fraught with risk, and can be an immense strain on people and resources. Data from the Standish Group CHAOS Report indicates that between 50% and 70% of large-scale ERP migrations fail to meet their original scope, timeline, or budget objectives. It's relatively uncommon for organizations to adopt this method for significant system overhauls, and yet it is exactly what most B2B SaaS vendors implicitly assume their customers will do.
The vendor schedules "integration update" tickets for the migration weekend, throws engineers at it on Saturday, and discovers at 2 AM that the new tenant's REST endpoints return SOQL-style IDs while the old SOAP endpoints returned base-62 strings. For a B2B SaaS vendor connected to that ERP, a Big Bang cutover creates an engineering nightmare. The differences between the old system and the new system are not just cosmetic. They represent fundamental shifts in API architecture, much like the breaking API changes that plague standard SaaS integrations.
A few specific failure modes show up consistently in these projects:
- Protocol shifts: Moving from XML-based SOAP envelopes to JSON-based REST or GraphQL APIs.
- Schema drift mid-migration: Custom fields renamed during the move. Picklist values consolidated. Legacy custom fields (
custbody_invoice_approval_status) rarely map 1:1 to the new cloud schema (custom_fields.approvalStatus). Your mapping logic, hardcoded against the legacy schema, silently writes to the wrong field. - Auth model changes: Moving from long-lived Basic Auth credentials, token-based session auth, or static IP allowlists to short-lived OAuth 2.0 tokens or mutual TLS that require automated refresh cycles. Your secrets store and refresh logic now need two parallel implementations.
- Pagination and rate-limit shifts: SOAP-era APIs that returned 10,000 rows per call get replaced with stateless, cursor-based REST endpoints capped at 100 per page with strict concurrency limits. Sync jobs that ran in 12 minutes now take 6 hours.
- Dual systems of record: For weeks or months, both ERPs hold partial truth. Read-from-legacy plus write-to-new is a recipe for lost transactions. Read-from-both with no reconciliation is worse.
None of this is hypothetical - it is the standard texture of every NetSuite-to-NetSuite, Dynamics-AX-to-D365, or JDE-to-Fusion project. If your integration logic is hardcoded - if (erp === 'legacy_on_prem') { executeSoapCall() } - your engineering team is forced into a frantic, high-stakes rewrite. When the inevitable data model mismatches occur, your sync jobs fail, background workers crash, and customer trust evaporates.
The fix is not better cutover planning. The fix is to stop assuming a cutover at all.
Architectural Patterns: Strangler Fig and Dual Writes
To achieve zero downtime, system integrators rely on two proven architectural patterns: the Strangler Fig pattern and dual-write synchronization. The two patterns that actually work for zero-downtime ERP migration are well documented and battle-tested. They are also frequently misapplied.
The Strangler Fig Pattern, Applied to ERP Integration
The strangler fig pattern was introduced by Martin Fowler as a way to manage risk when modernizing or rewriting large, monolithic systems. The mechanism is simple: you replace specific functionality with a new service or application, one component at a time, and a proxy layer intercepts requests that go to the monolithic application and routes them to either the legacy system or the new system.
Applied to ERP integration, the "monolith" is the legacy ERP and the "new system" is the cloud ERP. Your SaaS product talks to an abstraction layer - a façade or a Unified API - that decides, per resource and per account, which backend gets the call.
flowchart LR
A[Your SaaS Product] --> B[Integration Façade]
B -->|invoices, journals| C[(Legacy On-Prem ERP)]
B -->|customers, vendors| D[(Cloud ERP)]
B -.read both, write new.-> E[Reconciliation Job]
C -.batch sync.-> DThis pattern limits the blast radius. If the new cloud API endpoint fails or returns unexpected data structures, you simply update the routing configuration to fall back to the legacy system. Your core application logic never changes. The phases typically look like this:
- Façade-only: All reads and writes still hit the legacy ERP. The façade just normalizes the response shape so your application stops caring about SOAP vs REST.
- Read from new, write to legacy: Low-risk resources (reference data: chart of accounts, currencies, subsidiaries) move first. Reads come from the cloud ERP, writes still go to legacy with replication catching up.
- Dual-write transactional resources: Invoices, sales orders, journal entries get written to both systems simultaneously, with the legacy system as system of record.
- Flip system of record: Cloud ERP becomes authoritative. Legacy becomes the fallback reader for historical data.
- Decommission: You can start deprecating the legacy database and legacy system. After you validate the new database, you can retire the legacy database. This retirement completes the migration process with minimal disruption.
Dual Writes Without Drift
While the Strangler Fig pattern handles routing, you still have the problem of state consistency. If a user creates a new contact in your SaaS application, that record must exist in whichever ERP is currently acting as the source of truth.
Dual-write infrastructure is essential for synchronizing data between legacy ERPs and modern cloud applications during a phased migration. Implementing bidirectional, near real-time writes ensures that both the old and new systems maintain consistent state until the legacy system is fully decommissioned. Microsoft's own Dynamics 365 dual-write infrastructure exists precisely because keeping two ERPs in agreement is non-trivial: you need bidirectional, near real-time writes, conflict resolution, idempotent operations, and a way to bound divergence.
A few rules that hold up in production:
- Idempotency keys on every write: Stamp each outbound call with a deterministic ID derived from the source event. If the same payment posts twice during reconciliation, the second write is a no-op.
- Asymmetric authority by resource: Pick exactly one system of record per resource type at any given moment. "Both" is not an answer.
- Compensating transactions, not rollbacks: Distributed two-phase commit across two ERPs is a fantasy. If a write succeeds against the cloud ERP and fails against legacy, queue a compensating action and surface the inconsistency.
- Reconciliation jobs, not just sync jobs: Run a periodic diff that compares record counts and checksums between the two systems. The first time you see a 0.3% drift on customer balances, you want to know within an hour, not at month-end.
When your application issues a POST request to create a record, the abstraction layer must fan out that request to both systems. This introduces significant complexity around rate limiting and partial failures. If the modern cloud API accepts the write, but the legacy on-premise API rejects it due to a rate limit, your systems are now out of sync.
It is critical to understand how your abstraction layer handles these failures. A robust platform does not automatically retry, throttle, or apply backoff on rate limit errors invisibly. When an upstream API returns an HTTP 429, the abstraction layer should pass that error directly to the caller, normalizing the upstream rate limit info into standardized headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset) per the IETF specification. Because the abstraction layer passes these normalized errors back, your dual-write infrastructure must implement its own resilient queuing. You need an event bus, exponential backoff, and strict idempotency keys to ensure that a failed write to the legacy system is retried until it succeeds, without duplicating the successful write in the cloud system.
How System Integrators Use Unified APIs to Abstract the ERP Layer
Building a custom abstraction layer to handle Strangler Fig routing and protocol translation is a massive undertaking. It requires mapping XML to JSON, normalizing pagination cursors, and maintaining separate OAuth clients. The pragmatic choice for most B2B SaaS teams is a declarative Unified API that already implements the proxy, the normalization, and the per-account routing.
The core property you want is this: changing which underlying ERP an account points to should be a configuration change, not a deploy. If your SaaS code calls GET /unified/accounting/invoices?integrated_account_id=acme_co, the same call should work whether acme_co is currently connected to NetSuite, SAP S/4HANA Cloud, or Microsoft Dynamics 365 Business Central.
Modern system integrators use a declarative Unified API to abstract the ERP layer entirely. By placing a zero data retention platform like Truto between the SaaS application and the customer's infrastructure, you decouple your codebase from the underlying API specifics. The platform does not use hardcoded switch statements to figure out if it is talking to a legacy NetSuite SOAP endpoint or a modern REST API. Instead, integration behavior is defined entirely as data - JSON configuration blobs that describe how to talk to the API, and JSONata expressions that describe how to translate the payloads.
When a request hits the unified endpoint, the generic execution engine loads the configuration for the currently active connected account. If the account is pointed at the legacy ERP, the engine reads the configuration, formats an XML envelope, applies Basic Auth, and fires the request.
// Caller code stays identical across the entire migration.
const invoices = await truto.unified.accounting.invoices.list({
integrated_account_id: 'acme_co',
modified_after: lastSyncTimestamp,
})
// Behind the façade, acme_co's connection moves from
// legacy_netsuite_soap -> netsuite_rest -> sap_s4hana_cloud
// without a single line of caller code changing.When the customer is ready to cut over to the cloud ERP, the system integrator simply updates the connected account configuration to point to the new integration definition. The core SaaS application continues to make the exact same request. The execution engine now reads the new configuration, formats a JSON payload, applies the OAuth 2.0 bearer token, and fires the request.
A practical separation of concerns for the migration window looks like this:
| Concern | Where it lives |
|---|---|
| Resource shape (Invoice, Journal, Vendor) | Unified data model |
| Endpoint, auth, pagination per ERP | Integration config (data) |
| Field-level translation per ERP | JSONata mapping expression (data) |
| Per-customer custom field handling | Account-level override (data) |
| Routing decisions during cutover | Façade logic in the integration platform |
| Business workflow | Your SaaS application |
The single most expensive mistake teams make is putting any of the first four rows into application code. Once if (provider === 'netsuite_legacy') shows up in your billing service, you have lost the abstraction. This turns a high-risk code deployment into a low-risk data operation. You can read more about how this architecture eliminates technical debt in our guide on Zero Integration-Specific Code: How to Ship API Connectors as Data-Only Operations and The Architect's Guide to Bi-Directional API Sync.
The Proxy API Fallback: Even with powerful JSONata transformations, you will occasionally encounter legacy ERP edge cases that simply do not fit into a unified model. A customer might rely on a highly specific, undocumented stored procedure in their on-premise system or a custom SuiteScript endpoint that has no equivalent in the modern cloud API. For these scenarios, abstracting the connection should not mean losing access to the underlying power of the native API. Truto provides a Proxy API that gives direct, unmapped access to the integration's endpoints. The Proxy API handles the authentication lifecycle, credential injection, and URL construction, but skips the mapping layer entirely. You will need it more often than you think during a migration. Read more in What is a Proxy API?.
Handling Custom Fields and Schema Drift During the Transition
The hardest part of an ERP migration is not the authentication or the protocol change. It is the schema drift.
Enterprises heavily customize their ERPs. The legacy NetSuite account has 47 custom fields on the customer record, six of which are referenced by the cloud-side workflow. The S/4HANA target has those same concepts but renamed, retyped, and partially consolidated. During the migration window, both shapes are live, sometimes for the same logical customer.
If your SaaS application expects a unified data model, this schema drift will break your sync jobs. You need a way to map these disparate custom fields dynamically, on a per-customer basis, without altering your core application logic. The cleanest pattern is a three-level mapping override hierarchy powered by JSONata, a functional query and transformation language for JSON.
The 3-Level Override Hierarchy
To manage these transformations across hundreds of enterprise customers, Truto utilizes a strict 3-level override hierarchy. This allows system integrators to map custom fields differently for the legacy ERP versus the cloud ERP on a per-account basis during the migration window.
- Platform default mapping: "Standard NetSuite invoice maps to standard unified Invoice." This works for standard, out-of-the-box fields across all integrations.
- Environment override: Overrides applied to a specific deployment environment. Your integration team adds custom field mappings that apply across all your customers using NetSuite. This catches your product-wide enrichment.
- Account override: Overrides applied directly to a single customer's connected account.
The account-level override is the key to zero-downtime migrations. While Customer A is still on the legacy ERP, they use the default mappings. When Customer B (Acme Co) migrates to the cloud ERP, you apply an account-level override to their specific connection. Acme Co's custcol_legacy_po_ref field maps to purchase_order_reference on the unified Invoice for that account only.
// Example Account-Level Override for a legacy custom field
{
"integrated_account_id": "acme_co",
"unified_model_override": {
"accounting": {
"invoices": {
"list": {
"response_mapping": "$merge([$, { 'purchase_order_reference': custom_fields.custcol_legacy_po_ref }])"
}
}
}
}
}When Acme finishes their migration, you flip a flag and the override targets the new field name on the cloud ERP, handling any type conversion on the fly.
// Example Account-Level Override targeting the new cloud custom field
{
"integrated_account_id": "acme_co",
"unified_model_override": {
"accounting": {
"invoices": {
"list": {
"response_mapping": "$merge([$, { 'purchase_order_reference': customFields.purchaseOrderReference }])"
}
}
}
}
}During cutover for Acme Co, that single JSONata expression flips from reading custom_fields.custcol_legacy_po_ref (legacy NetSuite path) to reading customFields.purchaseOrderReference (cloud path). No other customer is affected. No SaaS code is redeployed. This means you can support customers in various stages of their migration journeys simultaneously, without maintaining multiple code branches. See 3-Level API Mapping: Per-Customer Data Model Overrides Without Code and Mapping Custom Objects with JSONata for working examples.
This architecture handles schema drift gracefully. It does not handle semantic drift. If "customer" in the legacy ERP includes prospects but the cloud ERP separates them, your unified model needs an explicit decision about which semantics to expose. Ship that decision in the unified data model design, not in the mapping layer.
A note on rate limits during cutover: dual-writes effectively double your API call volume against both systems. Most cloud ERPs have stricter concurrency limits than their on-prem predecessors. As mentioned, a good unified API surface normalizes upstream rate-limit headers and passes 429s straight through, leaving retry and backoff to the caller. Only your application knows whether a stalled invoice sync is a hard failure or a candidate for delayed retry. For practical patterns, see our guide on Handling Rate Limits Across Multiple Third-Party APIs.
Future-Proofing Your Enterprise Integrations
Enterprise IT is not static. ERP migrations are not one-time events. The customer who is moving off legacy NetSuite this year is the same customer who will move from S/4HANA Cloud to whatever Oracle ships next decade. In five years, these "modern" cloud APIs will be deprecated in favor of new architectures, new authentication protocols, and new data models.
If you hardcode your B2B SaaS application to specific third-party APIs, you are signing up for an endless cycle of forced rewrites, broken syncs, and frustrated enterprise customers. Every time an upstream provider changes their system, your engineering roadmap gets hijacked. The strategic move is to stop treating each migration as a one-off integration project and start treating every ERP connection as inherently mutable.
Three commitments make that real:
- Code your SaaS against unified resources, never against vendor-specific endpoints. The day a salesperson promises a new ERP, your engineering response should be a config change, not a sprint.
- Adopt the Strangler Fig and dual-write patterns by default, even when no migration is announced. The Strangler Fig pattern allows for the gradual replacement of the old system, reducing risk and making the process more manageable, and because the system is replaced piece by piece, the legacy system can continue to operate during the transition. The same property keeps your SaaS reliable through a customer's planned and unplanned changes.
- Push integration configuration outside your release cycle. If updating a single account's mapping requires a deploy, your blast radius is wrong. Per-account overrides stored as data are the smallest unit of change you can ship.
The customers who complete enterprise ERP migrations without canceling your contract will be the ones whose vendors absorbed the change quietly. Stop letting your customers' IT migrations dictate your engineering schedule. Abstract the connection layer, treat integrations as data operations, and build a system that survives the next decade of enterprise upgrades.
FAQ
- What is the Strangler Fig pattern in ERP migrations?
- It is an architectural approach where a legacy system is gradually replaced by a new system, piece by piece. An abstraction or proxy layer intercepts API calls and routes specific domains to the new cloud system while the old on-premise system remains active for other domains.
- How do dual writes prevent data loss during cloud migration?
- Dual-write infrastructure ensures that every data mutation (like creating an invoice or updating a customer) is written to both the legacy on-premise ERP and the new cloud ERP simultaneously, maintaining state consistency until the cutover is fully complete.
- Why do "Big Bang" ERP cutovers fail for SaaS integrations?
- Big bang migrations require every dependency to switch atomically over a weekend. Unanticipated schema drift, authentication model changes (like moving from Basic Auth to OAuth 2.0), and rate-limit shifts between the legacy and cloud ERPs almost always cause hardcoded integrations to break when business operations resume on Monday.
- How do unified APIs handle schema drift between legacy and cloud ERPs?
- A unified API uses a three-level override hierarchy (platform, environment, account) combined with JSONata transformations. This allows engineers to dynamically map disparate custom fields for a single customer's connection without touching core application code or affecting other users.