Modelling Integration Points Before You Build Them
The integration almost always gets built the same way. Someone opens a ticket that says "Portal needs to call the new Payments API," a developer writes the client code, points it at a URL, handles the happy path, and ships it. Three weeks later somebody asks who owns the shared customer reference, whether the call is synchronous, what happens when the payments provider has an outage, and whether this was supposed to go through a security review because it crosses out to a third party. Nobody wrote any of that down, because nobody had to. The first artifact that described the integration was the code itself, and code is a terrible place to have an architecture conversation after the fact.
The integration that never got designed
Most integrations are decided in a sentence and built in a sprint. "We'll just call their API." "We'll subscribe to the event." "We'll read straight from their database, just for now." Each of those sentences is actually five or six decisions compressed into one — a protocol, a direction of dependency, an assumption about who owns the data on each side, an assumption about whether the caller can tolerate waiting, and an unstated bet about what happens when the other side is down. None of those decisions get written anywhere. They get inferred, months later, by someone reading a client class and reverse-engineering what the original author must have been thinking.
This isn't a discipline failure so much as a tooling gap. There has never been a cheap, fast place to write down "we are about to add a dependency from A to B, here is what kind of dependency it is" before the dependency exists in running code. The alternatives on offer are both wrong for the job. One is to skip design entirely and let the PR be the design document, which means the review happens after the interfaces are already built and changing course is expensive. The other is to route every new integration through a formal architecture board — a meeting, a template, a queue — which is heavy enough that people quietly route around it for anything that doesn't look big enough to justify the wait, and "just calling their API" never looks big enough until it's the thing paging someone at 2 a.m.
There's a middle path, and it doesn't require a committee: model the integration as a relationship, with its properties filled in, before a single line of client code exists. That relationship is small enough to write in minutes, precise enough to be checked by a machine, and visible enough that a colleague can read it in thirty seconds and object if something's wrong. In Mooodels that relationship is a real object in the same model as everything else the architecture already describes — not a paragraph in a wiki that nobody will find again.
A relationship is where the decision actually lives
An integration is, structurally, a relationship between two elements that already exist or are about to. What makes it worth modelling isn't the arrow — an arrow on a whiteboard says almost nothing — it's the properties that arrow can carry. A relationship in a semantic model isn't just "A talks to B." It's a place to attach the actual decisions: protocol, purpose, ownership, consistency expectations, what happens under failure. A first pass at a new integration between a citizen portal and an external payments provider is a single relationship, from Portal to PaymentsProvider, with half a dozen properties filled in: HTTPS as the protocol, "submit payment for permit fee" as the purpose, the payments team as the owner, synchronous as the consistency expectation, external as the boundary, and "queue and retry, show pending status to citizen" as the fallback behaviour.
Nothing about that is exotic. It's the same handful of facts a competent engineer would eventually write into a design doc, a comment, or an incident postmortem — just moved to before the build instead of after the outage. The difference is that once it's a relationship in the model rather than prose in a document, it stops being inert. It can be queried, checked against rules, included automatically in views, and picked up by impact analysis the moment someone asks "what depends on the payments provider" six months from now. A paragraph in a design doc can't answer that question at all; someone has to remember the doc exists and go read it.
None of this requires a new element type or a special "integration" object. It's the same relationship mechanism Mooodels uses for every dependency in the model — application to database, service to service, portal to API. What changes is which properties get filled in and how deliberately, because a new external dependency deserves more scrutiny than a call between two components that have talked to each other for years.
Four questions the model can force you to answer first
The value of writing the relationship before the code isn't the form it takes, it's that a short list of properties acts as a checklist nobody can quietly skip. Four questions in particular tend to be the ones that get discovered in production rather than decided on purpose. Modelling the integration first means somebody has to type an answer into a property, and a blank or missing property is easy to flag with a rule — which is a much better failure mode than a blank assumption nobody notices until it matters.
Who owns this data?
Every integration touches some piece of data that both sides can now see, and exactly one side should be authoritative for it. When Portal starts writing permit applications into a system that PermitAPI also writes to, or when two services both cache a copy of a customer's address, ownership has to be assigned on purpose or it gets assigned by accident — usually to whichever system happened to write last. An owner property on the relationship, naming the team or system of record, turns a question that normally gets settled by archaeology ("whichever table looks more complete is probably the real one") into a fact that's visible before the integration exists. It also gives impact analysis something to check later: if PermitDB is ever retired, every relationship that names it as the owner of some data is exactly the set of things that need a new home for that data, not a guess.
Is this synchronous or asynchronous?
This single property changes almost everything else about how the integration behaves under load and under failure, and it's routinely left implicit because "just call the endpoint" reads as synchronous by default without anyone deciding it should be. A consistency property of synchronous versus asynchronous forces the decision into the open, and forces it early enough that it can still be changed cheaply. A synchronous call from Portal to a payments provider means a citizen is sitting on a spinner for however long that provider takes to respond, and it means Portal's own availability is now capped by the payments provider's availability. An asynchronous version — submit the request, get an acknowledgement, poll or get notified later — changes the failure story completely, and changes what the frontend has to be built to show. Deciding this after the client code is written usually means deciding it by finding out the hard way, during an incident, that a slow downstream call was quietly making the whole portal feel slow.
Does this cross a security boundary?
Not every dependency is equal, and the ones that leave the organization's own perimeter — to a third-party API, a partner system, a public network — carry review requirements the ones that stay inside usually don't: data residency, contractual obligations, a different threat model entirely. A boundary property of internal or external on the relationship means this question gets answered at the moment the dependency is proposed, not discovered later by a security team doing an audit and finding an undocumented call going out to a third party. It also means a view that filters for boundary: external gives a security reviewer an exhaustive, always-current list of everything crossing the perimeter — a list that used to require someone manually maintaining a spreadsheet that was accurate on the day it was written and nowhere close to it a year later.
What happens if the downstream system is unavailable?
This is the question that's cheapest to answer before the integration is built and most expensive to answer after, because after usually means during an incident. A fallback property doesn't need to be sophisticated — "queue and retry, show pending status to citizen" is a perfectly good answer for a payments call, "fail the request, this is not degradable" is a perfectly good answer for something that genuinely can't proceed without its dependency. What matters is that the answer exists and is visible before anyone writes a try/catch block, rather than being invented under pressure the first time the payments provider actually goes down. A relationship with no fallback property at all is itself useful information: it tells a reviewer that either nobody has thought about this yet, or the honest answer is "the whole feature breaks," and either way that's worth surfacing before the build starts rather than after.
From property to rule: validating the proposal before anyone builds it
A property that anyone can leave blank is a suggestion. A property that a rule checks is a gate. Once ownership, consistency, boundary, and fallback are real fields on a relationship, they stop being things a reviewer has to remember to ask about and become things the model can check automatically, the same way a linter checks a code style rule. A few rules that are easy to state once the properties exist and next to impossible to enforce consistently without them:
- Any relationship with
boundary: externalmust have a non-emptyfallbackproperty. No exceptions silently allowed — if the honest fallback is "there isn't one," that has to be written, which at least makes it a conscious decision instead of an oversight. - Any relationship with
boundary: externalmust use an approved protocol — HTTPS, not a plaintext protocol, not a direct database connection reaching out past the perimeter. - Any relationship touching an element tagged
PIImust declare anowner, so a data-protection question always has an answer to point to. - Any relationship into an element already tagged Tier-1 that is
synchronoustriggers a flag for review, because a new synchronous caller changes that Tier-1 system's blast radius under load.
These rules run the moment the relationship is proposed, not after the integration ships. That timing is the entire point. A rule that fires during a quarterly architecture audit tells you about a problem that's already been running in production for months; a rule that fires on a proposed relationship tells you about a problem before a single test has been written against it, when fixing it costs a changed property instead of a changed API contract with a partner who has already integrated against the old one.
Impact analysis on a relationship that doesn't exist yet
Rule checks answer "is this integration well-formed." Impact analysis answers a different, equally important question: "given everything else already in the model, is this a good idea." Because the proposed relationship lives in the same graph as every other element and relationship in the architecture, it can be analyzed exactly like an existing one before it's committed to.
Concretely, that means answering questions like these against the draft, not against a mental model of the system: does adding Portal → PaymentsProvider create a cycle with something that already calls back into Portal? What's currently downstream of PermitAPI, and does adding one more synchronous caller change what "PermitAPI is degraded" means for the rest of the landscape? Is PaymentsProvider, once this relationship exists, suddenly a single point of failure for two unrelated citizen-facing flows that never used to share a dependency? None of these questions require the integration to exist in running code to be answerable — they only require the relationship to exist in the model, with its properties filled in, which is exactly the artifact that gets produced before anyone opens an IDE.
This is also where a proposed integration earns scrutiny proportional to what it actually touches, rather than proportional to how it was pitched in a stand-up. A relationship between two low-traffic internal tools that nobody else depends on can be waved through in minutes. A relationship that adds a new synchronous dependency onto a system twenty other things already rely on deserves the extra five minutes of impact analysis, and the model can tell the difference automatically — a human reviewer working from a verbal description of "we're adding a call to the payments API" usually can't, because they don't have the rest of the graph in their head.
Propose, validate, then build — not a euphemism for a meeting
None of this requires a new review process bolted onto an existing one. The workflow is: propose the relationship, with its properties, as a change to the model; run the rule checks and impact analysis against the proposal; have a human — the person who'll actually own the integration, not a board — look at what came back; then, and only then, start writing the client code. That's a design review, but it's a design review that takes the shape of a diff instead of a meeting.
An AI assistant fits into this workflow at exactly one point, and it's a narrower one than it might sound. Given a plain-language description — "Portal needs to call the new payments provider synchronously to submit a fee, this goes out to a third party" — it can draft the relationship and a reasonable first pass at the properties: protocol, boundary, a suggested fallback based on similar relationships already in the model. What it produces is a ModelPatch, a proposed, reviewable change, not a modification applied to the model directly. A person reads the diff, corrects whatever the assistant guessed wrong — the actual owner, a fallback that reflects a decision only a human can make — and approves it before it becomes part of the model. The assistant is useful for getting a first draft of the boring-but-necessary fields onto the page fast; it is never the one deciding what those fields should say.
That review is genuinely fast, because there's very little to disagree about. A relationship with six properties and a rule-check result attached is something a colleague can read and object to in under a minute, the same way reviewing a five-line diff is faster than reviewing a five-hundred-line one. Compare that to what "design review" usually means for a new integration: a slide, a meeting on someone's calendar next Tuesday, a verbal description of the plan that may or may not match what actually gets built.
| Architecture board | Model a relationship first | |
|---|---|---|
| What gets reviewed | A slide or a verbal description of the plan | The actual relationship, with real properties, as it will exist |
| Turnaround | Days to weeks, bound to a meeting slot | Minutes, asynchronous |
| What's checked | Whatever the reviewers happen to think to ask | Every rule that applies, every time, plus impact analysis |
| What happens to small integrations | Usually skipped, because the process is too heavy to bother with | Same lightweight step regardless of size |
| Record afterward | Meeting notes, if anyone wrote them | The relationship itself, permanently part of the model |
The heavyweight process fails the integrations that matter most in one particular way: it's expensive enough that people route small, "obviously fine" integrations around it entirely, and those are exactly the ones that turn out not to be fine six months later, because nobody who could have caught the missing fallback or the accidental Tier-1 dependency was ever asked to look.
Worked example: a new payments integration, start to finish
Put the whole sequence together on one concrete case. Product wants citizens to pay a permit fee inside the portal instead of being redirected to a separate payment page. The plan, as described in the ticket, is "call the new external payments provider from Portal when the fee is due."
Before any client code exists, the relationship gets drafted in the model: Portal to PaymentsProvider, over HTTPS, for the purpose of submitting a permit fee, owned by the payments team, synchronous, crossing an external boundary. Six facts, none of them controversial, all of them written down in the same place the rest of the architecture already lives.
The rule check for boundary-crossing relationships runs immediately and fails: boundary: external with no fallback property set. That's the first thing caught, and it's caught in seconds, against a draft, not against a shipped feature. The person owning the integration adds the missing decision — queue the request and retry it, showing the citizen a pending status in the meantime — and with that property filled in, the rule passes.
Impact analysis on the draft turns up something the ticket didn't mention: Portal already has one synchronous external dependency, to an identity verification service, and this would be the second. Neither call is individually alarming, but a reviewer looking at the graph — not at the ticket — notices that Portal's actual availability ceiling is now the combined uptime of two third parties it doesn't control, stacked on the same user-facing action. That's not a reason to block the integration; it's a reason to make the synchronous choice a deliberate one instead of a default one, and it's exactly the kind of thing that's obvious once the relationship sits next to everything else in the model and invisible when it's described verbally in a stand-up.
Only after this — properties filled in, rule passing, impact analysis reviewed — does anyone open an editor and write the client that actually calls the payments provider. The interface the developer builds to now has an explicit, agreed contract to build to: synchronous, HTTPS, with a defined behaviour when the provider doesn't respond. None of that had to be reconstructed from a Slack thread three months later, because it was never only in a Slack thread to begin with.
What this catches that a code review doesn't
A code review is a good filter for a lot of things — is this correct, is this tested, does this follow the team's conventions — and a poor filter for architectural questions, for a structural reason: by the time there's a pull request, the shape of the integration has already been decided. Reviewing the PR for "should this really be synchronous" is reviewing a decision that was made weeks earlier and is now expensive to unmake, because unmaking it means rewriting the client, possibly renegotiating with whoever owns the other side, and re-testing everything downstream of the change. A reviewer who raises that question at PR time is, realistically, choosing between rubber-stamping a decision they're not sure about or being the person who blocks a nearly-finished feature over something that should have come up a month earlier.
Modelling the relationship first moves that same question to the one point where changing the answer is still cheap — before there's a client, a contract, or a test suite built around the choice. It doesn't replace code review; it removes architectural judgment calls from code review's job description and puts them somewhere they can be caught while they still cost nothing to fix.
"Isn't this just an ADR with extra steps?"
An architecture decision record captures the same kind of reasoning, and where teams already write good ADRs, this isn't a replacement so much as a more useful storage format for exactly one category of decision: a new dependency between two systems. An ADR is prose, filed somewhere, that a person has to remember exists and go read. A relationship with properties is a queryable fact sitting in the same graph as the impact analysis, the rule checks, and every view of the architecture that will ever be generated — it shows up automatically in a landscape diagram, in a list of everything crossing a security boundary, in an answer to "what does the payments provider affect if it goes down," without anyone having to go find the document first. For decisions that don't map onto a relationship — why we chose this vendor, why this pattern over that one — an ADR is still the right tool. For "we are adding a dependency from A to B, here is what kind," the model can carry the decision as a structured fact instead of a paragraph, which is a strict improvement for anything that later needs to be queried rather than just read.
"Doesn't this slow a team down for small, obvious integrations?"
For genuinely small, internal, low-stakes calls, filling in six properties on a relationship takes less time than writing the ticket describing the work, and the rule checks that matter — boundary crossing, PII ownership, Tier-1 exposure — simply don't fire, because none of them apply. The step scales with what the integration actually touches, not with how big the ticket looks. What it prevents is the failure mode where "this is obviously fine" turns out, on the one occasion it wasn't, to have been the integration that took down two unrelated features because it happened to add a second synchronous dependency onto something already carrying load nobody was tracking. The cost of asking the four questions on every integration is a few minutes. The cost of not asking them on the one that mattered is usually an incident.
Design review as a property panel, not a process
The pattern underneath all of this is the same one that shows up everywhere a semantic model replaces a diagram: a decision that used to live in someone's head, or in a document nobody reliably reads, becomes a property on a real object that can be checked, queried, and shown automatically wherever it's relevant. An integration is a relationship. A relationship can carry ownership, consistency, boundary, and failure behaviour as first-class facts instead of implicit assumptions. And a fact that's explicit before the build starts can be validated by a rule and analyzed for impact in the minutes it takes to draft it — which is a much better place to catch a bad assumption than the incident that eventually surfaces it.
None of this requires slowing engineering down with process for its own sake. It requires treating "propose a relationship, check it, then build" as the default shape of adding a dependency, the same way a type system makes certain classes of mistake impossible to ship rather than merely possible to catch in review. Mooodels treats the integration point itself — not the diagram of it, not the document describing it — as the thing worth getting right first, because everything downstream of that decision, the code, the tests, the on-call runbook, is easier to write correctly once the direction, the protocol, the ownership, and the failure mode were settled on purpose instead of inferred after the fact.
See the model this article describes, working in a real editor.
Try the live demo