Nobody sets out to build a reconciliation process. It arrives as a side effect of three sensible purchases: a CRM for the sales team, an invoicing tool for the finance team, and an accounting package for the books. Each is good at its job. The work that appears afterwards is the work of keeping three copies of the same business in agreement, and it is unbounded, because it scales with transaction volume rather than with company size.
This post is about where the copies actually diverge, why a sync schedule cannot close the gap, and what specifically disappears when the record is shared instead of copied.
Five things every system keeps its own copy of
Integration diagrams are drawn as arrows between boxes. The failures are not in the arrows, they are in the duplicated fields. Five duplicates cause nearly all of the pain.
| Entity | Copy in the CRM | Copy in invoicing or accounting | How the divergence shows up |
|---|---|---|---|
| Contact and billing address | Account record, edited by sales | Customer record, edited by finance | Invoice sent to an address changed three weeks ago |
| Product or SKU | Catalogue item on the deal | Item on the invoice line | A renamed or retired SKU invoices under the old description |
| Price and discount | Negotiated on the quote | Rate on the invoice line | Invoice bills list price after an approved discount |
| Tax treatment | Often absent entirely | Determined at invoice time | Exempt customers taxed, or taxable customers not |
| Payment status | Read to decide whether to chase | Written when cash is applied | A paid customer receives a dunning email |
Note the direction of each one. Contact, product and price flow from the commercial side toward finance. Tax is usually determined only in finance. Payment status flows the other way, from finance back to the people talking to the customer. A sync that is reliable in one direction and best-effort in the other still breaks, and it breaks on the leg nobody tested.
The failure, in one afternoon
Here is the concrete case. A customer pays an invoice by bank transfer on Thursday morning. The finance system applies the payment at 11:20 and marks the invoice settled.
The CRM holds its own payment_status field, refreshed by a sync that runs nightly at 02:00. At 14:00 on Thursday, an automated dunning sequence reads the CRM copy, sees unpaid, and emails the customer a reminder with a late-payment warning. The customer forwards the reminder to their accounts payable team, who confirm the transfer, and someone on your side spends twenty minutes proving the payment was received.
Nothing malfunctioned. The sync ran on schedule. The dunning automation read the field it was designed to read. The finance system recorded the payment correctly. Every component behaved exactly as specified, and the customer still got a wrong email, because for fifteen hours the business held two different answers to the question “has this invoice been paid” and the automation happened to ask the stale one.
You cannot test this away. The only variable is the length of the window, and any window longer than zero eventually contains an event that cares.
Why nightly sync produces drift rather than delay
A scheduled sync is usually described as latency: the data is correct, just late. That description is too generous. Four mechanisms turn latency into permanent divergence.
Conflicting edits. Both sides change the same field inside one window. The sync applies a conflict rule, usually last-write-wins by timestamp, and one edit is destroyed. Nobody is told which.
Partial failure. A sync run pushes 400 of 500 records and then hits a validation error on record 401. The first 400 are committed. The remaining 99 are not attempted. There is no transaction spanning the two systems, so there is no rollback, and the next run starts from a state that is neither the old one nor the new one.
Silent schema mismatch. A field added on one side has nowhere to land on the other. The sync maps what it knows and drops the rest without error, because dropping unmapped fields is normal behaviour.
Replay and duplication. A webhook redelivered after a timeout creates a second payment record unless every consumer is idempotent. Idempotency is easy to specify and easy to forget on the fourth integration.
Each mechanism produces a difference that the next sync does not correct, because the next sync has no memory of what the correct value was. That is the definition of drift: divergence that persists after the mechanism meant to fix it has run.
What a shared record actually removes
The alternative is not a faster sync. It is not having a second copy to synchronise.
When a deal, its invoice, the stock movement it triggered and the journal entry it posted are rows in one database, a set of questions stops existing:
- Which system owns the customer’s billing address? There is one address row.
- What happens if the invoice writes but the ledger entry fails? They are one transaction, so both happen or neither does.
- How do we handle a conflicting edit? Two edits to one row serialise at the database, as they would anywhere else.
- How long until the CRM knows the invoice was paid? It already knows, because it is reading the same row finance wrote.
This is not a claim that unified software is better at everything. It is a narrower and more defensible claim: the reconciliation class of failure is caused by duplication, and removing the duplicate removes the class. The same reasoning underlies why an AI agent needs a system of record rather than a context window. An agent reading a stale copy makes confident, wrong decisions faster than a person does.
Where an integration is still the right shape
Draw the boundary honestly. External systems with their own authority over their own data should be integrated, not absorbed: payment processors, bank feeds, carriers, marketplaces, tax authorities. Their data is genuinely theirs, the direction of truth is clear, and you have no ability to unify anyway.
What benefits from unification is the internal boundary between your customer, your order, your invoice and your ledger. That boundary usually exists for procurement reasons rather than architectural ones, which is why it is worth removing and why removing it feels disproportionately effective.
There is also a useful middle pattern for the external cases. Oneop’s bank reconciliation keeps the bank line and the posted ledger line as two rows joined by a match score rather than merging them into one record, and a person confirms, unmatches or excludes each match before the reconciliation is finalised. Two rows are correct there, because the bank’s record and yours are genuinely different assertions that should be compared rather than assumed equal.
How Oneop is built for this
Oneop runs on one PostgreSQL database with one tenant boundary: 329 tables, of which 275 carry a tenant_id. A deal in the CRM, the invoice it became in finance and the stock movement it triggered are rows in that one database, not records synced between separate services.
The mechanical proof is the reverse direction. Voiding an invoice reverses its ledger entry and releases the stock it had reserved, in the same action, because both live in the same transaction boundary. No integration is capable of that guarantee, and no integration should be blamed for not providing it.
External connections still exist and are listed on the integrations directory, where they belong: processors, stores, carriers and accounting exports that genuinely live outside your workspace. The distinction the architecture makes is which boundaries are real.
For the currency-specific version of the divergence problem, where two systems hold two exchange rates for the same invoice, see multi-currency accounting. For the document-level version, where the invoice quantity and the shipped quantity come from two different processes, see invoicing from what shipped.