A NetSuite and Salesforce sync that survived quarter-end
A distributor closed deals in Salesforce and fulfilled them in NetSuite, with a person in the middle rekeying every order. Two previous integration attempts had been switched off after they silently duplicated records. We rebuilt it as a proper bidirectional sync with conflict rules, idempotent writes, and a replayable error queue, and it held through its first quarter-end.
- Scope
- Bidirectional order, account, and inventory sync
- Timeline
- 6 weeks, including a 2-week parallel run
- Delivery
- Source and runbooks handed to the client's platform team
- Volume
- About 2,400 orders a month, peaking near 400 a day
01The mess before
Every order was typed twice
A deal closed in Salesforce, then someone opened NetSuite and rekeyed the customer, the line items, the pricing, and the shipping terms. On a normal day that was a few hours of work. At quarter-end it was the reason orders sat unfulfilled while the sales team celebrated closing them.
Two previous integrations had been switched off
Both earlier attempts synced happily until something went wrong mid-batch, then retried from the start and created a second copy of everything already written. Finance found the duplicates weeks later during reconciliation. After the second incident the team turned the automation off and went back to typing.
Nobody knew which system was right
Sales edited addresses in Salesforce. Operations edited the same addresses in NetSuite. There was no rule about which one won, so whichever had been touched most recently by a human was treated as correct, and shipments went to old addresses often enough for it to be a running complaint.
02What we wired together
NetSuite 03How the build ran
-
Wrote down which system wins, field by field
Before any code, we produced a table: for every synced field, which system is authoritative, what happens when both change inside the same window, and what a human sees when the rule cannot decide. Getting sales and operations to agree on that table took longer than building the sync and prevented most of the problems the earlier attempts had.
-
Made every write idempotent
Each record carries a deterministic external id derived from the source record. Writes are upserts keyed on that id, so replaying the same event produces the same single record. This is the specific thing that had broken both previous integrations, so it was built first and tested hardest.
-
Modelled sync as a durable workflow
Each order sync is a Temporal workflow with explicit steps: map, validate, write the customer, write the order, write the lines, confirm. A failure at step four resumes at step four. There is no scenario where a partial write gets retried from the beginning.
-
Ran in parallel against the manual process
For two weeks the sync wrote to a NetSuite sandbox while people kept rekeying into production. Every difference between the two was investigated. That surfaced eleven mapping edge cases, including a tax treatment nobody had mentioned because it only applied to one customer.
-
Gave operations a queue they could drive
Anything the rules cannot resolve lands in a review queue with the two conflicting versions side by side and a one-click resolution that writes back and replays the workflow. Failures stopped being a support ticket and became a task in someone's day.
04Under the hood
Events, not polling, which changed the latency story
The earlier attempts polled Salesforce every fifteen minutes, which meant a floor of fifteen minutes plus batch time and a lot of wasted API calls against a governed limit. Driving the sync from Change Data Capture events dropped typical latency to a few minutes, cut API consumption by roughly 80 percent, and removed the quarter-end failure mode where the poll window filled up faster than a batch could drain it.
The dead-letter queue is a product surface
Most integrations treat failures as logs. Because operations needed to act on them without engineering, the queue shows the payload, the rule that rejected it, the exact field in conflict, and a resolve action. About 1.5 percent of orders land there, mostly new customers with incomplete tax data, and they clear in minutes rather than sitting invisible until reconciliation.
Replay tests against real payloads
We captured several thousand historical order payloads, including the malformed ones, and CI replays them against the mapping layer on every commit. Field mappings are the part of an integration that silently rots as both systems get customized, and this is what makes a regression fail the build instead of failing a customer.
Currency and unit conversion held explicitly
The two systems disagreed on rounding for multi-currency orders, which produced small invoice differences finance had been correcting by hand. Conversion now happens in one place with an explicit rounding rule agreed with finance, and the resulting figure is written to both systems rather than calculated twice.
05Before and after
| Metric | Before | After |
|---|---|---|
| Time from closed deal to live sales order | About 9 hours | About 4 minutes |
| Orders rekeyed by hand each month | About 2,400 | About 36, only the queue exceptions |
| Duplicate records created | Recurring, found at reconciliation | None since go-live |
| Salesforce API calls per day | About 41,000 from polling | About 8,000 event driven |
Orders move themselves. Sales closes a deal and operations sees a sales order minutes later with the right customer, the right pricing, and the right tax treatment. The team that used to spend quarter-end rekeying now spends it fulfilling, and the fifteen or so orders a week that genuinely need a decision arrive in a queue built for exactly that.
Have a build like this one?
Book a scoping call →