Tech Reads
ERP & Enterprise Systems9 min read

The ERP Integration Patterns That Actually Hold Up at Scale

We have integrated with Odoo, Business Central, SAP S/4HANA, and a few bespoke ERPs nobody should be running in 2025. Most of the patterns are the same regardless of platform. Here is what survives — and one pattern that has caused a production incident on every single system where we have seen it used.

The post-mortem from a bidirectional sync

A distribution client was running a WMS (warehouse management system) alongside their Odoo ERP. Both systems needed to know the current inventory count. The integration team built a bidirectional sync — when inventory changed in Odoo, the WMS was updated. When inventory changed in the WMS, Odoo was updated.

It worked for four months. Then one Tuesday morning, a WMS batch job and an Odoo inventory adjustment ran simultaneously on the same SKU. Both systems wrote their value. Both systems received the other's update and wrote back. The sync loop ran 847 times before a circuit breaker tripped and froze both systems.

When the freeze resolved, the inventory record for 12 SKUs was in an indeterminate state. Nobody knew which system held the correct number. The resolution required a physical stock count on those SKUs — three days of warehouse team time — and a full re-reconciliation of the affected records.

This is the pattern that kills systems: bidirectional sync without a canonical source of truth. The question "which system is correct when both have changed?" has no automated answer. Every integration that tries to answer it automatically will eventually produce a loop, a conflict, or corrupted data.

847sync loop iterations before the circuit breaker tripped — three days of warehouse time to resolve the corrupted inventory records that resulted

Pattern 1: event-driven, not polling

Polling — asking the ERP "has anything changed since I last checked?" on a schedule — is the default approach because it is easy to build. It is also consistently wrong in ways that compound over time.

Polling introduces latency proportional to your poll interval. If you poll every 5 minutes, every downstream system is 5 minutes behind. For a WMS that needs near-real-time inventory, a 5-minute lag is a restock decision made on 5-minute-old data. For a financial system that needs confirmed PO status, a 5-minute lag means approvals queue up and rush through together.

Polling also creates thundering herd problems at scale. Fifty concurrent poll jobs hitting the ERP API simultaneously every 5 minutes saturate the connection pool. We have seen this cause ERP slowdowns that were diagnosed as capacity issues — they were polling architecture issues.

The alternative: webhook-first design. When something changes in the ERP, the ERP pushes an event to your integration layer. Your integration layer processes it within milliseconds. No latency, no load spike, no thundering herd. Odoo supports webhooks natively from version 16 onward. Business Central has Power Automate triggers. SAP S/4HANA has the Event Mesh. The polling fallback is for ERPs that do not support webhooks — use it only when necessary, and document it as technical debt with a plan to replace it.

Pattern 2: idempotent writes with correlation IDs

Every write to an ERP from an integration layer should be idempotent: sending the same write twice produces the same result as sending it once, with no duplicate records, no double-processing, no error.

This matters because network failures are real. A webhook fires, your integration layer processes it and sends a write to the ERP, the network drops before the ERP confirms receipt, your layer retries. Without idempotency, you now have a duplicate purchase order in the ERP. With idempotency, the retry is a no-op.

The mechanism: every write carries a correlation ID — a deterministic identifier derived from the source event (typically a hash of the source system ID and the event timestamp). Before writing, the ERP integration checks whether a write with this ID has already been processed. If yes, return the prior result. If no, process and record.

Our take

Not all ERPs support idempotency keys natively. Microsoft Dynamics 365 Finance has request deduplication via the x-ms-deduplication-id header. Odoo does not have native deduplication — build it in your integration layer, not in the ERP. The integration layer owns the correlation tracking; the ERP just receives writes.

Pattern 3: retry queues separate from main processing

When an integration write fails — ERP is temporarily unavailable, rate limit hit, validation error — the natural instinct is to retry immediately in-line. Do not do this.

Inline retries block the main processing queue. If the ERP is down for 20 minutes, your main queue backs up with retrying items, new events cannot be processed, and when the ERP comes back you have a backlog that floods the system simultaneously. This is how a 20-minute ERP outage becomes a 4-hour operational disruption.

The pattern we use: failed writes go to a separate retry queue immediately, with an exponential backoff schedule (retry after 30 seconds, 2 minutes, 8 minutes, 30 minutes, 2 hours). The main queue continues processing new events without interruption. The retry queue runs independently and has its own circuit breaker — if the ERP is down, retries pause automatically rather than hammering a unavailable endpoint.

Items that exhaust the retry schedule — typically after 24 hours of attempts — move to a dead letter queue with a human review notification. The dead letter queue is visible to operations, has enough context to understand why the item failed, and has a one-click "retry now" when the underlying issue is resolved.

The canonical source of truth problem — solved

Back to the WMS/ERP inventory disaster. The fix is simple in principle and requires discipline in practice: define a canonical source of truth for every data entity, and make the integration flow one-directional from that source.

Inventory levels

WMS owns inventory. ERP receives inventory updates from WMS on a defined schedule. ERP never writes back.

Purchase orders

ERP owns POs. WMS receives PO data from ERP when confirmed. WMS never creates POs.

Goods receipts

WMS records physical receipt. ERP receives goods receipt confirmation from WMS to match against PO.

There is no situation where both systems update the same entity. One system is always the authority. The integration is always a read-from-source, write-to-receiver flow. When the business says "both systems need to be able to update this," the answer is to choose which system is the authority and redesign the workflow in the other system to submit requests to the authoritative one.

Every time someone says "both systems need to be able to update this," ask what happens when both update it at the same time. If the answer is "we will handle conflicts," you are building the same system that failed 847 times.

What changes by platform

The patterns above are platform-agnostic. The implementation varies:

Odoo: The XML-RPC API is stable but old. The JSON-RPC API is faster and preferable for new integrations. Webhooks work well for outbound events from Odoo 16+. Bulk operations via the API are rate-limited by the server configuration — know your limits before building high-volume sync.

Business Central: The OData API is well-documented and reliable. The AL extension model means you can build custom API endpoints if the standard ones do not expose what you need — this is the right approach rather than trying to query tables directly. The sandbox environment is genuinely useful for integration testing.

SAP S/4HANA: The API landscape is complex — SOAP BAPIs, REST OData, iDocs. The Event Mesh is the right architecture for real-time integration. We have spent weeks understanding which API endpoint maps to which business object before writing a line of integration code. Do not skip this step.

Share

Related reading