Two-way sync sounds like the perfect solution: keep two systems aligned so the team can work wherever is most convenient. In practice, “just sync it” turns into duplicate records, overwritten fields, and long afternoons trying to figure out which system is correct.
The good news is that most sync failures are not caused by fancy edge cases. They come from missing decisions: what “same record” means, who owns each field, and what happens when both systems change at once.
This post lays out a durable pattern small teams can implement without building a full data platform. It focuses on clarity and recovery: if something goes wrong, you can explain it, detect it, and fix it.
Why data drift happens in two-way sync
Data drift is what you see when two systems that should match slowly diverge. It usually shows up as mismatched statuses, stale addresses, or notes that appear in one tool but never make it to the other.
Drift happens for a few predictable reasons:
- No stable identity: the sync matches by name or email, so small edits create “new” people or companies.
- Ambiguous ownership: both systems allow edits to the same field, so the “last write wins” and someone loses.
- Partial updates: one system sends only changed fields, the other expects full objects, and older values leak back in.
- Hidden constraints: one system normalizes values (like phone numbers) or rejects them, so round trips mutate data.
- Missed changes: webhook failures, pagination bugs, or time window errors cause gaps.
A solid pattern does not attempt to prevent every failure. It makes failures visible and recoverable, and it prevents the worst kind of failure: silently corrupting good data.
Set ownership rules: who is allowed to change what
The most important design decision in two-way sync is not “how often do we poll?” It is field ownership: which system is authoritative for each piece of information.
Start by splitting your data into three buckets:
- System A owns: values that should only be edited in A (for example, billing terms in the accounting system).
- System B owns: values that should only be edited in B (for example, lead source in the CRM).
- Shared but controlled: values that can be edited in either place, but need clear conflict rules (for example, contact phone number).
Keep this simple. If you cannot confidently explain ownership to a teammate in one minute, your rules are too complex to enforce reliably.
Concrete example: service business CRM and invoicing
Imagine a small service business with a CRM for customer communication and an invoicing system for billing. They want both systems to show the current customer address and whether a job is complete.
A workable ownership map could be:
- CRM owns: contact name, email, phone, marketing consent, conversation notes.
- Invoicing owns: tax settings, invoice status, payment status, ledger codes.
- Shared: postal address and “job complete” status.
With this approach, the sync is not “bi-directional for everything.” It is “bi-directional for a defined subset,” which is how you avoid accidental overwrites.
Track changes with a small change log
Two-way sync becomes manageable when you stop thinking in terms of “copy the record” and instead treat updates as change events. You do not need a full event streaming platform. You need a durable record of what you last saw and what you last applied.
At minimum, maintain a local mapping and a cursor for each system:
- Identity mapping: System A ID ↔ System B ID, plus your internal ID.
- Last seen version: a timestamp, incrementing version, or “updated_at” value per system.
- Last applied fingerprint: a hash or checksum so you can detect no-op updates.
{
"internal_id": "cust_10492",
"a_id": "A-8821",
"b_id": "B-55109",
"a_updated_at_last_seen": "2026-07-01T10:12:00Z",
"b_updated_at_last_seen": "2026-07-01T10:09:00Z",
"last_applied_hash": "c2f8... (canonical fields only)",
"ownership": {
"address": "shared",
"payment_status": "b",
"lead_source": "a"
}
}
The key is the canonical fields only idea. Hash only the fields you intend to sync. This prevents unrelated fields (like a system-specific tag) from triggering noisy updates.
With this small change log, you can answer essential questions: Did we see the update? Did we apply it? If we applied it, what did we send?
Resolve conflicts deterministically
Conflicts are inevitable: two users edit the same shared field in two different tools before the next sync cycle. If your system does not define conflict behavior, you will get random outcomes based on network timing.
Deterministic conflict handling means you can predict the result. It also means you can explain it to your team, which matters when they are the ones dealing with customer-facing data.
Three conflict strategies that work
- Ownership wins: if a field is owned, the owner overwrites the other system. Simple and often best.
- Last writer wins with guardrails: compare update timestamps, but only for fields you explicitly mark as shared and only if both timestamps are trustworthy.
- Merge by type: for append-only data like notes, you can merge by adding a new entry that includes the source and time.
Most small teams should prefer “ownership wins” and use “last writer wins” sparingly. Timestamps are frequently misleading when systems have different precision, time zones, or delayed writes.
When a conflict is detected, log it as a first-class outcome. Do not silently overwrite. Even if you auto-resolve, you want an audit trail for later debugging.
Add reconciliation and observability
Even with good ownership rules, syncs fail in boring ways: expired tokens, changed permissions, rate limits, and payload validation errors. A “set it and forget it” sync will drift unless you plan for detection.
Build two layers: a continuous sync loop and a periodic reconciliation.
- Continuous sync loop: processes incremental changes. It should be safe to retry and should not create duplicates.
- Periodic reconciliation: runs daily or weekly, samples or fully compares records, and repairs gaps.
Reconciliation is where you catch missed webhooks or broken “updated since” logic. If you cannot reconcile, you will not notice drift until a human reports it.
For observability, you do not need elaborate dashboards. You do need a few counters and alerts:
- Number of changes fetched from each system per run
- Number of changes applied successfully
- Number of conflicts detected
- Number of records that failed validation and require attention
If you publish internal operational docs, keep a short runbook nearby so anyone can answer “is the sync healthy?” in five minutes.
Copy-paste build checklist
Use this checklist as a lightweight spec before you write any integration code. If you cannot check these boxes, you are likely building drift into the system.
- Define record identity: what uniquely identifies a record in each system, and how you map them.
- List sync fields: enumerate only the fields you intend to keep aligned.
- Assign ownership per field: A owns, B owns, or Shared (and write down which shared strategy you will use).
- Define conflict rules: what happens when both sides change between sync cycles.
- Normalize data: decide on canonical formats (phone, address lines, casing) before hashing or comparing.
- Track last seen state: store per-system cursor or updated_at markers and a canonical hash of applied fields.
- Design failure handling: how you retry, where you park failed records, and how you avoid duplicates.
- Plan reconciliation: how often you compare, what “mismatch” means, and how you repair.
- Define success metrics: expected change volume, acceptable error rate, and alert thresholds.
Common mistakes
- Syncing “everything”: teams mirror entire objects instead of a curated subset, which increases conflicts and coupling.
- Using names as keys: matching “Acme LLC” across systems guarantees duplicates when someone edits formatting.
- Relying on timestamps alone: clock skew and delayed writes cause incorrect last-writer outcomes.
- Ignoring deletions: one system deletes or merges records, the other keeps dangling references forever.
- No audit trail: if you cannot show what you sent and why, debugging becomes guesswork.
A quick rule: if a human cannot reconstruct what happened from your logs and stored metadata, you will end up re-running jobs blindly and hoping the data improves.
When not to do a two-way sync
Two-way sync is powerful, but it is not always the right choice. Do not force it if the underlying workflow does not support shared editing.
Avoid two-way sync when:
- One system is clearly primary: if 95 percent of edits happen in one tool, use one-way sync plus deep links.
- Edits must be tightly controlled: regulated or high-risk fields should have a single system of record.
- Data models differ significantly: if one system has one “customer” and the other has multiple related entities, you will spend most of your time mapping edge cases.
- You cannot reconcile: if API limitations prevent comparing or listing records, drift is guaranteed.
In these cases, a simpler pattern often wins: pick a system of record and synchronize outward, or create a thin internal UI where edits happen once.
- Two-way sync succeeds when you define field ownership and keep the synced surface area small.
- Track “last seen” and “last applied” state so you can detect misses and avoid accidental overwrites.
- Conflicts should be deterministic and logged, even if they are auto-resolved.
- Reconciliation is not optional. It is the safety net that catches silent drift.
Conclusion
Two-way sync does not have to be fragile. If you treat it as a product with explicit rules, state tracking, and reconciliation, a small team can keep systems aligned without constant manual clean-up.
Start with ownership, then build the smallest reliable loop: identity mapping, incremental updates, deterministic conflict behavior, and a periodic recon job. Everything else is optimization.
FAQ
How often should a two-way sync run?
As often as your workflow needs, but not faster than your ability to observe and recover. Many teams do fine with near-real-time webhooks plus a periodic poll, then daily reconciliation for safety.
Can I rely on webhooks only?
Webhooks are great for latency, but they are not a completeness guarantee. You still want reconciliation, because deliveries can fail, be delayed, or be disabled during maintenance.
Why use a hash of canonical fields?
A canonical hash helps you detect whether a sync would be a no-op and prevents you from re-applying the same change repeatedly. It also reduces noise when systems add metadata you do not intend to sync.
How should I handle deletions?
Prefer “soft delete” semantics when possible: mark records inactive and keep an audit trail. If you must hard delete, store tombstones so the other side does not recreate the record during reconciliation.