In short
A Reading-based logistics and warehousing SME was running three disconnected systems - a warehouse management system, an accounting platform, and a CRM - with orders, stock movements and financial data being re-keyed by hand between them. Six weeks after we shipped a custom API-driven integration layer, order-processing time was down 65%, manual reconciliation work had effectively vanished, and the project paid for itself inside six months. This piece is about the architecture, the three patterns that made it boring (which is the goal), and what we would change next time. The client-facing version of this story is on the warehouse ERP integration case study page; this is the engineering breakdown.
The starting point
The client moved freight for a living. Their warehouse staff used a dedicated WMS to pick and dispatch; their finance team used an accounting platform to invoice; their account managers used a CRM to track customer activity. None of the three knew the others existed. Every order touched at least one human re-keying data into a second system, often two. Stock figures could drift between WMS and CRM by half a day. Invoices regularly went out before the warehouse had marked the order shipped, leading to awkward customer calls.
The brief was unromantic: do not migrate any of the three systems, do not retrain the staff, but make the data flow between them as if it had been one platform all along. Our job was to build the connective tissue and disappear into the background.
The constraint that drove every architectural decision was this: the WMS was the only system that knew what was physically happening on the warehouse floor. Anything we built had to treat it as the source of truth for stock and dispatch, with the CRM and accounting platform as downstream followers - never the other way around.
The architecture, in one picture
┌────────────────┐ events ┌──────────────────┐ write ┌────────────────┐
│ WMS │ ─────────────► │ Middleware │ ──────────► │ Accounting │
│ (source of │ │ (API + queue + │ │ │
│ truth) │ ◄───────────── │ validator) │ ◄────────── │ │
└────────────────┘ invoices └──────────────────┘ contacts └────────────────┘
│
▼
┌──────────────────┐
│ CRM │
│ (read-only mirror│
│ of orders + │
│ customer events)│
└──────────────────┘
The middleware was the only piece we built; the three product systems stayed exactly as they were. Three pipes ran independently:
- Stock and dispatch events from the WMS were pushed to the middleware on every meaningful state change (order picked, packed, shipped, cancelled). The middleware then fanned them out: an outbound write to the CRM (so account managers saw live status), and a different outbound write to the accounting platform (so invoices were only raised once the warehouse confirmed dispatch).
- New customers and contact-detail changes were the one place the CRM was authoritative. Those flowed CRM → middleware → accounting, so the finance team never had to set up a customer twice.
- Invoice settlement flowed accounting → middleware → CRM, so account managers could see outstanding balances and recent payments next to a customer's order history without ever opening the accounting tool.
There is nothing clever in any of that. The whole bet of the project was that boring, disciplined patterns would outperform anything novel.
Three patterns that made it boring (which is the goal)
1. Idempotent writes, keyed by the source system's id
Every outbound call to the CRM or accounting platform carried an externalReference equal to the WMS order id (or, for customer creation, the CRM contact id). The downstream system was responsible for the "create-or-update by external reference" logic. If a webhook fired twice - because the WMS retried, because the network blipped, because we redeployed mid-event - the second write was a no-op rather than a duplicate. We never had to write deduplication logic on our side.
2. Validate at the boundary, fail loud
The middleware ran every incoming event through a schema validator before doing anything with it. Missing fields, malformed dates, unknown SKUs - all rejected at the door, written to a dead-letter table, and surfaced on a small ops dashboard the warehouse manager checked twice a day. The result was that the integration almost never got into a half-finished state. Bad data was visible bad data, not silent bad data.
3. Outbox for every cross-system write
Nothing was posted directly from a webhook handler. Every outbound call to the CRM or accounting platform went into an outbox row first; a worker drained the outbox with exponential-backoff retry. When the accounting platform was down for two hours during a vendor maintenance window in week three, the warehouse never noticed - events just stacked in the outbox and drained when the platform came back up. The first incident report we wrote was a non-event.
Where it landed
Measured against the same period the previous year:
- −65% order-processing time, end-to-end from WMS dispatch to invoice raised
- ~0 manual reconciliation hours per week (down from a measured half-day across two team members)
- Full automation of order, stock and customer synchronisation - no scheduled batch jobs, no human in the loop
- 6 weeks from kick-off to go-live, including a week of parallel-running with the old manual workflow
- ~6 months to ROI, based on the labour cost the client was no longer spending on data re-entry
The interesting metric is the one we did not promise: data quality. Because the validator rejected malformed events at the door and surfaced them, the team started catching upstream data issues (typos in WMS product codes, missing CRM contact emails) far faster than they had before the integration existed. The middleware accidentally became a data-quality monitor for the rest of the business.
Questions we get about projects like this
How long did the integration take?
Approximately six weeks end-to-end: one week of discovery and API exploration with the three vendors, three weeks of build, one week of QA on a staging environment, and one week of parallel-running alongside the old manual workflow before we switched the old process off.
Was the system scalable for future integrations?
Yes - the middleware was designed from day one as a hub, not a point-to-point connector. Adding a fourth system (e.g. a shipping carrier API, a tax-reporting tool, a BI warehouse) is a matter of registering another consumer for the existing event stream rather than re-architecting. The client added a courier integration nine months later in under two weeks.
Did the staff need retraining?
No - that was an explicit constraint. The warehouse team continued to use their WMS exactly as before; finance continued to use their accounting platform exactly as before; account managers continued to use the CRM exactly as before. The integration was invisible to all three.
What we'd change next time
Two things. First, we would build the dead-letter dashboard before the integration went live, not in week eight after the first batch of malformed events. It is the kind of tool that feels optional until it is the only thing standing between you and a silent data-quality regression. Second, we would ship the outbox pattern from day one rather than after the first vendor outage. We got away with it - the outage happened on a Tuesday, with the outbox already in place because we had read about a similar integration where the team did not have one and learned from their mistake - but it could easily have gone the other way.
Neither is technically interesting. Both would have made the project boring even sooner, which is exactly what you want from this kind of work.
Considering a similar integration? See our ERP integration services or read our guide on how ERP integration works.