← All guides

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.

GTM Systems guide

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.

1 owner
Per field, per direction, or systems fight
~30%/yr
Data decay the sync must keep up with
0 undo
On a bad writeback to 40k records

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.

Field ownership map One owner per field, direction decided up front
EnrichmentfirmographicsMarketingcampaignsCRMdeals, pipelineWarehousefull historyone-wayone-wayreverse ETL slice
Enrichment owns firmographics and writes one-way into the CRM. The CRM owns deal and pipeline data. Marketing owns campaign membership. The warehouse holds the full history and reverse-ETLs only the activation slice back. Nothing is two-way unless a conflict rule exists.

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
Match the pattern to whether a delay changes the outcome. Polling frequently for slow-changing data and batching time-sensitive events are the two symmetric mistakes.
API calls per day as the polling interval shrinks
A 5-minute poll is 288 calls a day per job for data that may change once. Stack ten such jobs and you spend real quota to mostly hear nothing new. An event or webhook makes that cost near zero for the same freshness.
View as table
PointValue
Daily batch1 calls
Hourly24 calls
15 min96 calls
5 min288 calls
1 min1,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.

Integration build order: six decisions before a record flows
  1. 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

    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

    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

    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

    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

    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.