←  Case studies Manufacturing and distribution

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
9 hours → 4 minutes Time from a closed deal in Salesforce to a sales order live in NetSuite

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

Salesforce
Source of truth for accounts, opportunities, and pricing. Change Data Capture events drive the sync instead of polling.
NetSuite
Source of truth for inventory, fulfilment, and invoicing. Writes go through SuiteTalk with external ids on every record.
Temporal
Runs each sync as a durable workflow, so a crash mid-order resumes at the exact step it stopped instead of restarting the batch.
PostgreSQL
Holds the id mapping between systems, the sync journal, and the dead-letter queue for anything that needs a human.
TypeScript services
Field mapping, unit and currency transforms, and the conflict-resolution rules, all covered by tests against recorded payloads.
GitHub Actions
CI that replays a corpus of real historical payloads against every change, so a mapping regression fails the build.

03How the build ran

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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
Where it landed

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 →