Designing Integration APIs and Data Contracts Across Six Internal Systems
The unglamorous plumbing that keeps a multi-system marketplace coherent: shared vocabularies, versioned contracts, event flows, and the failure modes nobody warns you about.
2026-07-30 · By Filip Lauc
Why six systems need one shared vocabulary before they need APIs
Before writing a single endpoint, define a canonical vocabulary: what an order, delivery, product, and retailer actually mean, and which system owns each. Multi-system integration failures are rarely transport failures. They are disagreements about meaning that only surface once real data flows through.
On the Plodovi platform we built and maintained six interconnected systems: the public marketplace website, a retailer portal where producers manage catalogues and fulfilment, a shopper mobile app used during picking and delivery, a shopper portal, an admin application, and a business intelligence dashboard. Each one has a legitimate but different view of the same order. The marketplace cares about a basket and a payment. The retailer portal cares about the subset of lines that belong to that retailer. The picking app cares about physical units, substitutions, and weights that change at the moment of packing. The BI dashboard cares about revenue attribution over time, which means it needs the version of the order that was actually delivered, not the one that was placed.
The practical move is to write down these definitions as a glossary with owners before designing endpoints. An order line's quantity that is 'requested' versus 'picked' versus 'invoiced' should be three named fields, not one field whose meaning depends on which system last touched it. Every ambiguity you leave in the vocabulary becomes a reconciliation bug in the BI dashboard six months later, and reconciliation bugs are the most expensive kind because you have to correct history.
How to define a data contract that survives four years of change
A durable data contract specifies more than field names and types. It names the owning system, the identity key, required versus optional fields, allowed enum values and how new ones are introduced, nullability rules, timestamp semantics and timezone, monetary precision, and what a consumer must do when it receives something it does not recognise.
The single highest-leverage rule we apply is that consumers must tolerate unknown fields and unknown enum members. If the retailer portal crashes because the marketplace added a new fulfilment status, you no longer have six systems, you have one distributed system that must be deployed atomically. Additive changes should be safe by default, which means new fields are optional, new enum values map to a defined fallback behaviour, and no consumer validates strictly against a closed schema in production paths.
Money and units deserve their own paragraph in the contract. Fresh produce is sold by weight, so a line ordered as 'one bunch of kale' becomes 420 grams at picking, and the price changes accordingly. We settled on integer minor units for all monetary values, an explicit unit-of-measure field on every quantity, and separate fields for the estimated and final amounts, so no system has to infer whether a number has already been adjusted. VAT treatment was recorded per line rather than per order, because a single basket can mix rates.
- • Owner: exactly one system may write each field; everyone else reads.
- • Identity: a stable, system-generated ID plus any external references, never a composite of mutable attributes.
- • Time: UTC ISO-8601 timestamps, with separate business dates where a calendar day matters (delivery windows, cut-off times).
- • Money: integer minor units plus currency code, with tax treatment recorded at line level.
- • Quantity: value plus unit of measure plus a distinction between requested, fulfilled, and invoiced.
- • Evolution: additive-only by default, unknown fields and enum values tolerated, breaking changes require a new version.
Choosing between synchronous APIs and events for each interaction
Use synchronous request/response when the caller cannot proceed without the answer and the answer is authoritative right now. Use events when one system needs to tell others that something happened and can tolerate them reacting slightly later. Most integration pain comes from using the wrong one.
Checkout is the clearest synchronous case. Stock reservation, price validation, and payment authorisation must return a definitive yes or no before the shopper sees a confirmation, so those are direct calls with strict timeouts and idempotency keys. Order placed, order picked, delivery assigned, and delivery completed are the clearest event cases. The retailer portal, admin application, and BI pipeline all care about them, none of them need to block the shopper's checkout, and adding a seventh consumer later should not require touching the producer.
The trap in the middle is the read that looks synchronous but should not be. A BI dashboard querying live operational endpoints to build charts will eventually degrade the systems shoppers depend on, and it will produce numbers that shift between refreshes. We fed the dashboard from an append-only event and snapshot flow instead, so reports were reproducible and analytical load never competed with checkout. The same logic applies to the admin app: browsing and searching can read from a projection, while any state change goes through the owning system's API so business rules only live in one place.
Versioning, deprecation, and keeping a mobile app in the picture
Version at the contract boundary, not the endpoint level, keep old versions running until telemetry shows nobody uses them, and treat mobile clients as permanently outdated. A web app can be redeployed in minutes; an installed picking app on a driver's phone may be weeks behind, and that constraint shapes every breaking change.
In practice we made breaking changes rare by making them expensive to ourselves: any change that could break a consumer required a new versioned contract running in parallel, plus per-client usage logging so we could see exactly who was still calling the old shape. Deprecation then becomes a data-driven conversation rather than a guess. When a field genuinely had to change meaning, we introduced a new field name and let the old one keep its original semantics until it could be removed, which is far cheaper than a coordinated six-system release.
Contract tests are what make this sustainable. Each consumer keeps a set of examples of the payloads it depends on, and the producer's pipeline verifies it can still emit them. This catches the class of change that unit tests never see: someone tightens a validator, renames a nested field, or starts omitting a value that was previously always present. It also gives you a written record of what each system actually relies on, which is more accurate than any documentation that humans maintain by hand.
Handling the failure modes: idempotency, ordering, and reconciliation
Assume every message will be delivered twice, arrive out of order, or arrive after a related message it logically follows. Design for it with idempotency keys on every write, monotonic version numbers or sequence stamps on every entity, and a periodic reconciliation job that compares system state rather than trusting that every event landed.
Idempotency is straightforward once it is a rule: the caller generates a key, the receiver stores it with the result, and a repeat call returns the original result instead of creating a second order or a second charge. Ordering is harder. Rather than requiring strict global ordering, we attached a version to each entity and had consumers ignore any update older than what they already had. A stale 'order picked' event arriving after 'order delivered' becomes a no-op instead of a corrupted state.
Reconciliation is the part teams skip and later regret. Even with retries and dead-letter queues, some events fail permanently, and a marketplace that cannot explain a discrepancy between what a retailer was paid and what a shopper was charged has a business problem, not just a technical one. A scheduled job that compares aggregates across the operational systems and the BI store, then flags differences for a human in the admin app, turns silent drift into a visible ticket. Pair it with correlation IDs propagated across every system so one order can be traced end to end through logs when someone asks why a specific delivery looks wrong.
A practical sequence for designing contracts on a real project
Start from business events, not endpoints. Map the lifecycle of the core entity, decide who owns each state transition, then design the smallest contract each consumer needs. Endpoints designed from a database schema end up leaking one system's internals into five others, which is the change you can never undo cheaply.
This sequence works whether you are integrating six systems you are building yourself or connecting new services to software you inherited. The ordering matters more than the tooling: the choice between REST, GraphQL, or a message broker is far less consequential than getting ownership and vocabulary right first.
- • Map the lifecycle of your central entity end to end, listing every state and who causes each transition.
- • Assign a single owning system per entity and per field. Resolve every case where two systems both think they own something.
- • Write the glossary and the field-level contract, including units, money, time, and enum evolution rules.
- • For each interaction, decide synchronous or event-driven based on whether the caller must block on the answer.
- • Define idempotency keys, entity versioning, and correlation ID propagation before the first integration ships.
- • Add contract tests per consumer, per-client usage telemetry, and a reconciliation job with a human-visible output.
Key Takeaways
- • Integration failures across internal systems are usually disagreements about meaning, not transport problems. Agree on a shared vocabulary and single field ownership before designing endpoints.
- • Make additive change safe by default: optional new fields, tolerated unknown enum values, and no strict closed-schema validation in production paths.
- • Use synchronous calls only where the caller must block on an authoritative answer, and events for everything other systems merely need to know about.
- • Assume duplicate and out-of-order delivery. Idempotency keys, entity version numbers, and a reconciliation job with human-visible output are non-negotiable.
- • Treat mobile clients as permanently outdated, version at the contract boundary, and use per-client telemetry to decide when a version can actually be retired.
The systems and constraints described here come from our Plodovi case study, where we spent four years building and maintaining six interconnected systems for Croatia's leading short supply chain platform.
Frequently Asked Questions
Should I build a shared database instead of APIs between internal systems?
A shared database is faster to start with and much harder to change later, because every system becomes coupled to one schema and no one owns the business rules. It is defensible for two closely related services built by one team, but with several systems and separate release cycles you lose the ability to evolve any part independently. If you do share storage, still define explicit ownership per table and per field so writes stay in one place.
How do I document API contracts so they stay accurate?
Generate the specification from code or from a schema that the code validates against, so drift is impossible rather than merely discouraged. Supplement it with consumer-driven contract tests that record the exact payload shapes each system depends on, which doubles as documentation that fails your pipeline when it becomes wrong. Hand-written wiki pages describing endpoints go stale within a release or two.
What is the difference between an API contract and a data contract?
An API contract describes the interface: endpoints or message topics, request and response shapes, authentication, error codes, and rate limits. A data contract describes the meaning and quality of the data itself: field semantics, ownership, units, nullability, freshness expectations, and how the schema is allowed to evolve. You need both, and the data contract is the one that prevents the subtle, expensive bugs.
Do I need an event broker like Kafka for a project with only a handful of systems?
Usually not at first. A managed pub/sub service, a database-backed outbox with a worker, or even a well-structured job queue covers most needs at that scale with far less operational overhead. What matters is that you get idempotency, retries, dead-lettering, and ordering semantics right; those properties, not the specific broker, are what make event-driven integration reliable.
How long should we keep an old API version alive after releasing a new one?
Until telemetry shows real traffic has stopped, not on a fixed calendar. Log usage per client and per version so deprecation is a data decision rather than a negotiation. For mobile apps in particular, plan for a long tail: users on old builds may keep calling an old contract for months, so either support it or ship a forced-upgrade path deliberately.
Sources
Written by
Filip Lauc
CEO, Jaspero
Filip Lauc is the CEO of Jaspero, a software development agency based in Osijek, Croatia. A full-stack JavaScript developer with over a decade of experience across Angular, Svelte, and Node.js, he leads Jaspero's work as a long-term embedded engineering partner for clients like GlycanAge, where his team has served as the dedicated engineering team for six years.
Let's Build Together
Your vision,
our expertise.
From AI integration to full-stack development, we turn ambitious ideas into products that perform.