Key takeaways

  • Map systems of record and data ownership before designing any screen or database table.
  • Pick an integration pattern per flow based on latency, volume and failure tolerance, not habit.
  • Treat every integration as a product: versioned contract, owner, monitoring and a replay path.

TL;DR. In a mid-market company, most new software touches an ERP, a CRM, a payment provider, a warehouse system or all four. Projects that design the user interface first and "wire up the integrations later" tend to discover the hard constraints late. Integration-first architecture reverses the order: define who owns each piece of data, how it moves and what happens when it fails, then build the product on top.

The problem with screens-first

The usual sequence goes like this: a product team designs screens, engineers build a database to back them, and integration work is scheduled for the second half of the project. Then reality arrives.

  • The ERP can only accept order updates in a nightly batch, so the "live status" screen shows stale data.
  • The CRM and the new app both believe they own the customer's address, and they disagree.
  • A carrier API rate-limits at a volume you'll hit on your busiest day of the year.
  • A failed sync leaves an order half-created, and nobody gets alerted until a customer calls.

Each of these forces a redesign of something already built. Integration constraints shape the data model, the user experience and the operating model. They belong at the start.

Step 1: map systems of record

Before drawing any architecture diagram, write down every important business entity and the one system that owns it. This is your system-of-record map.

For a typical logistics or commerce business, it might look like this:

  • Customer: CRM owns identity and contact details. Billing system owns payment terms.
  • Product: ERP or PIM owns SKUs, pricing and descriptions.
  • Order: Order management system owns lifecycle and status.
  • Inventory: Warehouse management system owns stock on hand.
  • Invoice: Accounting system owns financial records.

The rule is simple: one owner per field. Other systems can hold copies, but only the owner accepts writes. When two systems both want to write the same field, you have found a design decision that needs an explicit answer now, not a sync conflict to debug later.

Step 2: choose a pattern per flow

There is no single right integration pattern. Each data flow has its own latency, volume and failure requirements, and the pattern should follow from them.

Pattern Good for Watch out for
Synchronous API call User-facing reads and writes that need an immediate answer (price check, address validation) Cascading failures and latency when the downstream system is slow
Webhooks or events Reacting to changes in near real time (order shipped, payment captured) Missed or duplicate deliveries; needs idempotency and a replay path
Message queue or event bus Decoupling many producers and consumers, smoothing load spikes Operational overhead; ordering guarantees vary by tool
Scheduled batch / file transfer High-volume, latency-tolerant flows (nightly ledger export, catalog sync) Stale data; large failures discovered hours late
Change data capture Replicating a legacy database with no usable API Couples you to the source schema; changes upstream break you

A common mistake is choosing one pattern for everything because the team is comfortable with it. A good design often uses three or four patterns, each where it fits.

Step 3: write the contract before the code

An integration contract is the agreement between producer and consumer. Write it down before either side builds anything. A useful contract covers:

  1. Schema. Field names, types, required versus optional, and examples. OpenAPI or AsyncAPI documents work well because they can generate tests.
  2. Identity. Which ID is canonical, and how IDs map between systems.
  3. Semantics. What each event or status actually means. "Shipped" in the warehouse system might mean "label printed," not "left the building."
  4. Volume and timing. Expected throughput, peak load and acceptable latency.
  5. Versioning. How changes are introduced and how long old versions are supported.
  6. Failure behavior. Retry policy, timeouts and what the consumer should do with a malformed message.

Contracts turn arguments about "the integration is broken" into checkable facts: either the message matched the contract or it didn't.

Step 4: design for failure as the normal case

Every integration will fail at some point. Partners have outages, credentials expire, someone renames a field. Integration-first design assumes this and plans for it.

Make operations idempotent

If the same message arrives twice, processing it twice should have the same effect as processing it once. Use idempotency keys on writes and check for duplicates on consumption. This one property makes retries safe and removes a whole class of double-charge and duplicate-order bugs.

Keep a dead-letter queue and a replay path

Messages that can't be processed after retries should land somewhere visible, not disappear. Operations staff need a way to inspect a failed message, fix the cause and replay it without engineering help.

Reconcile, don't just sync

Even good event-driven integrations drift. A scheduled reconciliation job that compares counts and totals between systems (orders created versus orders invoiced, for example) catches the silent failures that alerting misses.

Isolate slow dependencies

Put timeouts and circuit breakers around synchronous calls so one slow partner doesn't freeze your own application. Where possible, let users continue and confirm asynchronously.

Step 5: treat integrations as products

Integrations usually decay because nobody owns them. After launch, the team moves on, and the connector becomes a black box that only fails at 2 a.m.

Give every integration:

  • A named owning team, recorded in a service catalog.
  • Dashboards for throughput, error rate and lag, with alerts that go to the owner.
  • A runbook that says what to check first when it fails.
  • Contract tests in the CI pipeline of both producer and consumer.
  • A change process for partner-driven updates, such as API deprecations.

This is ongoing work, and it should be budgeted that way.

The takeaway

Business software is mostly the connections between systems. Design those connections first: map ownership, choose patterns deliberately, write contracts, assume failure and assign owners. The screens get easier because they sit on a foundation that behaves predictably.

A quick self-assessment

If you are planning a new platform or modernizing an old one, ask your team:

  • Can we name the system of record for every core entity?
  • Do we have written contracts for our five most important data flows?
  • Is every write operation safe to retry?
  • If a partner API is down for an hour, what happens to our users and our data?
  • Who gets paged when an integration fails, and do they have a runbook?

Any "we're not sure" answer is a risk worth addressing before the next build begins.

Fintrix designs and builds this kind of foundation through our system integration practice, often alongside cloud solutions work to host and monitor the integration layer. If your next project depends on systems that don't talk to each other well yet, talk to us about it.

Tell us what's slowing your team down

Message a senior engineer on WhatsApp. You'll leave with a clearer view of the problem, whether we work together or not.

Reply within one business day. No sales scripts.