Integrations & data flow
How systems sync without corrupting each other, plus the ownership, idempotency, and error-handling rules that keep a two-way integration from overwriting the truth every fifteen minutes.
The integration failure I think about most did not throw a single error. A marketing automation platform and the CRM were set to two-way sync on the lead-status field, and each system believed it owned that field. Marketing would set a lead to “Nurture,” the CRM sync would push “Working” back a few minutes later, marketing would overwrite it to “Nurture” again, and the two systems fought over one field roughly every fifteen minutes for months. No error logs, no failed jobs, just a field that flickered between two values forever and a set of routing rules that behaved randomly depending on which system had written last. Two-way sync without a clear owner is not an integration; it is a slow-motion collision.
Integrations are where the object model you built so carefully meets systems you do not control. Every connected tool has its own idea of what an account is, its own field names, its own sync cadence, and its own opinion about who owns the truth. The job of integration architecture is to make those systems agree on ownership and direction before a single record flows, because the failures here are the quietest and the most expensive: no error, just data that is subtly, continuously wrong.
Direction and ownership come before anything flows
The first question of any integration is not “how do we connect these” but “who owns each field, and which way does it flow.” A field can be one-way (system A writes, system B reads and never writes back) or two-way (both write, which is where the fights start). The safe default is one-way with a single named owner per field. Two-way sync is worth it only when both systems genuinely need to edit the same field and you have built a rule for resolving conflicts (last-write-wins, source-of-truth-wins, or a timestamp comparison). Most fields do not need two-way, and most two-way syncs exist because nobody stopped to ask whether they should.
Idempotency and external IDs keep replays safe
Every real integration retries. A network blip, a timeout, a webhook delivered twice: the same record will arrive more than once, and your integration has to treat the second arrival as harmless. That property is idempotency, and the way you get it is an external ID. Instead of the sync deciding “insert this record,” it does an upsert keyed on a stable external identifier, so the same payload landing twice updates the one existing record instead of creating a duplicate. Skip the external ID and every retry becomes a duplicate, every replay a data-quality incident.
Here is the SOQL I run before trusting any integration’s key: it finds records that landed without the external ID populated, which is the leading indicator that the sync is inserting instead of upserting.
-- Records the integration created without a stable external key:
-- these are the ones a replay will duplicate.
SELECT Id, Name, External_System_Id__c, CreatedBy.Name, CreatedDate
FROM Account
WHERE External_System_Id__c = null
AND CreatedBy.Name = 'Integration User'
ORDER BY CreatedDate DESC
Real-time or batch: match the pattern to the need
Not every sync should be real-time, and not every one should be a nightly batch. Real-time (webhooks, streaming, event-driven) is right when a delay changes an outcome: a hot inbound lead that needs routing in seconds, a closed-won that triggers provisioning. Batch (scheduled bulk sync) is right for volume that does not need to be instant: nightly enrichment refresh, warehouse loads, backfills. The mistake is polling every five minutes for something that changes once a day (burning API quota to mostly hear nothing) or batching something that needed to be instant (routing a hot lead six hours after it converted).
| Real-time / event | Scheduled batch | |
|---|---|---|
| Latency | Seconds | Minutes to hours |
| Best for | Hot leads, provisioning triggers, status changes | Enrichment refresh, warehouse loads, backfills |
| API cost | Low per event, spiky | Efficient in bulk, predictable |
| Failure mode | Duplicate or out-of-order events | Whole batch fails or partial-loads silently |
| Requires | Idempotent receiver, ordering tolerance | Error report, row-level retry, reconciliation |
View as table
| Point | Value |
|---|---|
| Daily batch | 1 calls |
| Hourly | 24 calls |
| 15 min | 96 calls |
| 5 min | 288 calls |
| 1 min | 1,440 calls |
Error handling is the integration, not a footnote
The part teams skip is what happens when a sync fails, and it is the part that decides whether the integration is trustworthy. A row that fails to write has to land somewhere visible (a dead-letter queue, an error object, an alert), not vanish into a log nobody reads. The integration I opened with was invisible precisely because it never failed loudly; the two systems just overwrote each other. Build the failure path first: every job reports what it wrote, what it skipped, and what it could not process, and a human sees the failures before a customer does.
- 1
1. Map ownership per field
For every synced field, name the one system that owns it. If two systems need to write it, define the conflict rule before you build.
- 2
2. Set direction, default to one-way
One-way with a single owner is the safe default. Two-way only when both systems must edit and a resolution rule exists.
- 3
3. Add an external ID and upsert on it
A stable key from the source system, marked unique and external. Upsert on it so replays and retries are idempotent.
- 4
4. Choose real-time or batch by latency need
Event-driven when a delay changes the outcome; batch for volume that tolerates it. Do not poll for slow-changing data.
- 5
5. Build the error path first
Failed rows land on an error object with a reason and a human gets alerted. A silent failure is worse than a loud one.
- 6
6. Reconcile on a cadence
Periodically compare source and target counts and key fields, so drift is caught by a report, not by a customer.
Integrations fail quietly, which is what makes them dangerous, so the whole discipline is about making ownership and failure explicit before anything flows. Name one owner per field and default to one-way. Give every synced object a stable external ID so retries are safe and replays do not duplicate. Match real-time versus batch to whether a delay actually changes an outcome. Build the error path first and reconcile on a cadence, because the integration that never errors is usually the one overwriting the truth every fifteen minutes. Do that and connected systems reinforce each other; skip it and they spend months fighting over a field while every routing rule downstream behaves at random.
Keep reading
All guides →Object & data model
How the standard objects, record types, and custom fields fit together, plus the grain rules and relationship choices that decide whether reporting ever ties out.
ArchitectCPQ & quote-to-cash
How quotes, price books, approvals, and billing connect into one revenue chain, plus the config choices that decide whether a signed deal actually invoices correctly.
ArchitectAutomation architecture
How flows, triggers, and validation rules coexist without fighting, plus the order-of-execution and consolidation rules that keep automation from eating itself.