Documenting a Microservices Architecture Without Drowning in Diagrams
Draw twelve services on a page and you get an architecture diagram. Draw sixty and you get a plate of spaghetti that nobody trusts, prints on a wall once, and quietly stops being updated. Microservices didn't invent bad architecture documentation — they just exposed how much of it was only ever surviving because nobody had reached the diagram's real limit yet.
Every team that moves from a handful of services to a real microservices estate hits the same wall twice, in two different directions, and usually within the same year. First the single "system landscape" diagram stops being readable. Then the fix everyone reaches for — draw more diagrams, smaller ones, one per service or per team — turns out to have its own failure mode, quieter but just as damaging. Neither problem is really about drawing tools. Both come from treating a diagram as the primary record of the architecture instead of as one view onto something that should exist independently of any picture.
The full-mesh diagram hits a wall
The math is not subtle. With n services, the number of possible direct relationships grows roughly with n(n-1)/2. At ten services that's forty-five possible edges — already busy, but a determined diagram can still separate them with careful layout. At forty services it's over seven hundred. Nobody draws seven hundred edges, and nobody needs to: most services only talk to a handful of neighbors. But the diagram doesn't know that in advance, so the person drawing it either omits relationships arbitrarily to keep the page legible, or draws everything and produces the thing every architecture team has seen taped to a wall at some point — a dense mesh of crossing lines that reads as "complex system" and nothing more specific than that.
The practical result is that the all-services diagram gets drawn once, usually during a platform kickoff or an audit, gets shown in a slide deck, and then is never touched again because updating it means re-untangling the whole thing rather than adding one line. It becomes a historical artifact — accurate about the architecture as it existed on the day someone had the patience to draw it, and progressively less accurate every week after that. Nobody deletes it, because it still looks authoritative. That's arguably worse than having no diagram at all: a wrong diagram that looks current actively misleads people who assume someone is keeping it maintained.
The alternative isn't better: one diagram per service
The natural response is to decentralize: instead of one impossible diagram, let each team own a diagram for their service, showing what it calls and what calls it. This scales better on the page — a single service with six or eight relationships is easy to draw clearly. It scales worse everywhere else.
Nobody owns the integration diagrams that sit between two services, so they either don't get drawn or get drawn once by whichever engineer happened to be building the integration, then never touched again by either team. A service's own diagram gets updated when that team remembers to, which in practice means: rarely, and never as part of the actual change that made it stale. An engineer on the Checkout team adds a new asynchronous call from OrderService to NotificationService to fire an email on refund. Updating the sequence diagram in Confluence is not part of the pull request template, not part of code review, and not something CI checks. The diagram doesn't fail loudly when it's wrong — it just sits there, published, findable, and quietly incorrect.
Multiply that by every team, every quarter, and eighteen months in you have a Confluence space full of diagrams that individually look authoritative and collectively disagree with each other and with the running system. Two diagrams both claim to describe how OrderService talks to PaymentService, drawn eight months apart by different people, and they no longer match — not because either author was careless, but because nothing connected the diagram to the actual dependency it was describing. The dependency changed in code; the picture of it didn't move.
Model every service once, as a real element with real relationships
The fix is not a better diagramming tool, and it isn't fewer or more diagrams either — it's not treating a picture as the primary record at all. Each service gets modelled once, as a real element with a stable identity, properties, and relationships that carry actual meaning. This is the same discipline a good OpenAPI spec or a service catalog entry enforces, applied to the dependency graph instead of just the service's own interface.
In Mooodels this looks like defining each service as an element — with its owning team, its criticality tier, and whatever tags the organization already sorts services by — and each dependency between them as a typed relationship. OrderService, owned by the Checkout team and tier 1, calls PaymentService synchronously over REST, calls InventoryService the same way, and publishes order events onto a topic that NotificationService consumes asynchronously. InventoryService, in turn, calls PricingService over gRPC. None of that is a picture yet. It's a record, and the pictures come later, from it.
Two things matter about capturing dependencies that way that a rectangle-and-arrow diagram doesn't naturally capture. First, the relationship carries real semantics — synchronous REST call, asynchronous event, gRPC — not just a line with an arrowhead that the reader has to interpret from context or a legend nobody checks. That distinction is not decoration: a synchronous chain of calls fails differently than an event that gets queued and retried, and any query about blast radius or deployment ordering needs to know which one it's looking at. Second, this is entered once, at the point the integration is actually built, by the team that knows the details — the same way a Dockerfile or an OpenAPI contract gets written once and then just is the current state, rather than being redrawn from memory every time someone wants to look at it.
Views become queries, not copies
Once every service and every dependency exists once in a real model, a diagram stops being a thing you draw and becomes a thing you ask for. A view is a query over the graph: start from one element, follow relationships out to some depth, render what comes back. Nothing about that view is a separate document that can drift — it's re-evaluated against the model every time it's opened, so it's current by construction rather than by someone remembering to update it.
The scoping parameter that matters most here is depth. A view rooted on one service, following relationships one hop in both directions, shows exactly what it calls and what calls it — the smallest useful unit for most day-to-day questions, and the one an engineer opens twenty times a week without thinking of it as consulting documentation at all.
Depth 2 pulls in one more hop each way — useful when the question isn't "what does this service touch" but "what's the blast radius if this service has a bad day," since a failure two hops away can still land on you through a chain of synchronous calls. A blast-radius view is usually that same two-hop query with one extra condition applied: keep only the relationships marked synchronous, and drop the asynchronous ones.
That extra condition is doing real work: for an incident, an async event queue that can absorb backpressure is a different risk profile than a synchronous call chain that propagates a timeout immediately. A diagram that shows both kinds of edge identically hides exactly the distinction an on-call engineer needs during an outage. A query can filter it out with a single condition because the relationship's mode was captured as data when it was modelled, not left implicit in an arrow style someone has to remember the meaning of.
This is also where the two failure modes from the start of this piece both stop applying at once. There's no all-services mesh, because nobody has to look at more than the relevant neighborhood for the question they're actually asking. And there's no separately maintained per-service diagram going stale in Confluence, because the "diagram" for any given service is just the depth-1 query re-run against the live model — there's nothing else to keep in sync.
Catching circular dependencies and coupling smells before they bite
Once dependencies are real, typed relationships in a graph rather than lines on independently drawn pictures, they can be checked, not just looked at. Two checks earn their place in almost every microservices estate: circular dependencies and services with too many direct callers.
Circular dependencies between services are rarely introduced deliberately — they accumulate. OrderService calls InventoryService for stock checks. Later, InventoryService starts calling PricingService to apply a promotional hold on reserved stock. Later still, someone on the pricing side adds a call back into OrderService to look up order history for a loyalty discount, and now there's a cycle: OrderService → InventoryService → PricingService → OrderService. No single engineer who made any one of those three changes could see the cycle — each of them only looked at their own service's direct dependencies, which is exactly what a per-service diagram would have shown them too, cycle and all invisible. A cycle like this turns into deployment-ordering headaches, retry storms that feed back into their own cause, and a debugging session where the root cause of a slowdown turns out to be three services waiting on each other.
Because the dependency graph is real data rather than a set of pictures, this is a graph algorithm, not a design review: walk the "calls" relationships and report any cycle.
rule "no-circular-service-dependency" {
scope: type = Service
check: no_cycles(relationship: "calls")
severity: error
}
The second check is a coupling smell rather than a hard error: a service with an unusually high number of direct callers has effectively become a shared dependency that every one of those callers now has an opinion about changing. That's sometimes intentional — an auth service or a shared identity provider is supposed to have many callers — but it's worth surfacing rather than discovering the hard way when a routine change to that service needs sign-off from a dozen teams and takes three sprint cycles to schedule.
rule "high-fan-in-coupling" {
scope: type = Service
check: incoming_count(relationship: "calls") <= 8
severity: warn
message: "Service has more than 8 direct callers — treat interface changes as a breaking-change process."
}
Both of these run over the whole model, every time, exhaustively — not on whichever diagrams happen to exist. A cycle that spans three services owned by three different teams, none of whom drew a diagram that included all three, is invisible to hand-drawn documentation almost by construction, because no single team's diagram was ever going to include a service two hops outside their own boundary. A graph query doesn't have that blind spot; it doesn't know or care whose diagram it would have been.
| Approach | Stays accurate over time | Finds cycles automatically | Flags coupling smells | Effort to maintain |
|---|---|---|---|---|
| One all-services diagram | Degrades within weeks past ~20 services | No — visually, if at all | No | High, and rarely paid |
| One diagram per service/team | Drifts per-diagram, invisibly | No — no team sees the whole graph | No | Distributed, unenforced |
| Modelled services, scoped views | Current by construction | Yes — a graph query | Yes — a graph query | Added at integration time, not after |
Onboarding: what does this service actually talk to
The clearest place this pays off day to day isn't an architecture review — it's a new engineer's first week. The question they need answered is almost always some version of "what does this service actually talk to," and the honest failure mode in most organizations is that the answer lives in three places that disagree: a Confluence diagram from a past reorg, whatever the tribal knowledge in the team happens to be, and the actual code, which is the only one of the three guaranteed to be current and also the least approachable for someone who joined two days ago.
Searching Confluence for "OrderService architecture" and finding a diagram is not the same as finding a correct one. The date in the corner might say it was last touched eighteen months ago; more often there's no date at all, and the new engineer has no way to know whether what they're looking at is current or a snapshot of an earlier version of the system. Asking a teammate works, but only surfaces what that teammate happens to remember, and pulls a second person's time away from their own work every time someone new needs the same answer.
Against a real model, the same question is a query, not a search. Root a view on the service the new engineer has been handed, follow outgoing relationships two hops, and read what comes back.
The answer comes back exhaustive with respect to what's modelled, instantly, and self-service — no one else's time gets pulled into answering a question that's going to get asked again by the next new hire in three months. The completeness of the answer becomes a question of modelling discipline, not search effort: if every integration gets a relationship added when it's built, the query is trustworthy. That's a much easier thing to sustain than expecting every team to remember to redraw a picture nobody else is checking.
Scaling this across a growing platform
None of this requires a big-bang modelling exercise before it's useful. A platform typically grows into needing this the same way it grew into having the problem in the first place — a handful of services at a time. The practical pattern that works is treating "add the relationship" as part of building the integration, owned by whichever team is doing the work, the same way that team already owns updating their own service's API contract. Nobody needs to model someone else's service; each team only ever adds the relationships that originate from changes they're making.
For teams that already have architecture tooling in place — Sparx EA or Archi repositories built up over years of governance work — the model doesn't have to start from zero or live in a second, disconnected place. Mooodels interoperates with both, so an existing service catalog can be a starting point rather than something to re-enter by hand, and a team standardized on C4 for their diagrams can keep working in those semantics through a C4 profile over the same underlying model rather than a completely different notation.
AI assistance fits into this the same way it fits into the rest of a model-native workflow: an engineer can describe a new integration in plain language — "OrderService now calls the new FraudCheck service synchronously before confirming an order" — and get back a proposed change to the model, a reviewable patch that adds the service and the relationship with the right type and direction, rather than a paragraph of prose that still has to be manually translated into a diagram edit by someone. Nothing gets applied to the model until a person approves it, which matters here specifically: the model is the thing every cycle-detection rule and every scoped view depends on being correct, so it's worth having a human confirm the relationship is real and directional before it becomes load-bearing.
Who commits the relationship, and where it lives
A shared canvas that every team edits at once has its own version of the staleness problem: two people move the same boxes around in the same session, or one team's in-progress edit gets overwritten by another's save, and the "current" diagram becomes whichever save happened last rather than whichever one was actually correct. That's a bad fit for an organization where twenty teams are each adding a handful of relationships a month, mostly not touching the same part of the graph at the same time.
A model that serializes to plain, diffable files sidesteps this the same way source code does. The Checkout team's change — a new relationship from OrderService to a fraud-check service — is a small, readable diff: one new element, one new edge, nothing else touched. It goes through the same pull request their code change goes through, gets reviewed by someone who can see exactly what was added rather than having to compare two exported diagram images, and merges independently of whatever the Payments team is doing to their own corner of the graph the same afternoon. There's no lock on a shared file, no "someone else has this diagram open," and no risk that a rebase silently drops a relationship because two people happened to touch the same file in a binary format that doesn't diff meaningfully.
This is also what makes the earlier cycle-detection and fan-in rules practical to run continuously rather than as an occasional audit: if the model lives in version control alongside the services it describes, the same rules that check for circular dependencies can run in CI on every pull request that touches the graph, catching a newly introduced cycle at review time instead of three months later when someone finally goes looking for why a deployment keeps stalling.
Two objections worth taking seriously
"Won't this just rot the same way the diagrams did?" is a fair question, and the honest answer is that it can, if adding a relationship is treated as an optional extra step disconnected from the actual work of building the integration. The difference in practice is that it's a much smaller step than redrawing a diagram, it can be checked — a rule flagging any service with zero modelled relationships and no explicit "isolated" tag catches the ones nobody got around to — and it happens once per integration rather than once per diagram that happens to reference that integration. It's not immune to neglect; it's considerably more resistant to it than a picture nobody owns.
"Don't we still need nice-looking diagrams for leadership and for external audiences?" — yes, and a scoped view doesn't stop being presentable just because it's generated rather than hand-drawn. A depth-1 or depth-2 view can still be laid out, styled, and exported for a slide deck; the point was never that generated views have to look worse, only that there's exactly one source feeding all of them, so the version shown to leadership and the version an engineer queries at 2 a.m. during an incident are guaranteed to agree, because they're the same graph, asked two different questions.
The underlying shift is small to describe and large in effect: stop trying to keep pictures of the architecture up to date, and instead keep the architecture itself — as a real, queryable graph of services and typed relationships — up to date, then generate whatever picture a given question needs on demand. The all-services mesh and the eighteen-month-old Confluence diagram are both symptoms of the same mistake: treating a drawing as the record. Model the services once, scope the views to the question being asked, and let rules watch the graph for cycles and coupling smells that no single team's diagram was ever going to catch on its own.
See the model this article describes, working in a real editor.
Try the live demo