Outgoing links are what a service claims. Incoming links are what actually happened.
👋 Hi, I'm Anton - a software engineer working mostly in PHP/Symfony and Go, currently carving a live PHP monolith into Go services. This block is about what a service is to everyone else: whose it is, who calls it, what it promises. This part is about the links between services - specifically, about which direction of a link you are allowed to believe. Running notes are on my GitHub: github.com/brilliant-almazov.
This is how I do it right now, with the price attached - maybe you already do it better, maybe you see it differently.
Where the links are supposed to live
Short primer, so the rest reads without context from the previous part.
Each of our Go services raises itself from one declaration - a manifest that lists its daemons, its databases, its queues and its schedules. That file is not documentation next to the code; it is what the runtime and the deploy pipeline read. A service card - the page that answers "what is this service" - is a rendering of that declaration rather than a wiki page somebody maintains.
Seven rows go on that card. One of them is incoming links: who calls this service. That row is the subject here, and it is the one row that cannot be filled in from the manifest, because the manifest belongs to the wrong service. Your dependencies are declared by you. Your callers are not.
The one line that moves an arrow
Here is the failure class, stated without an incident wrapped around it.
An architecture diagram - the good one, the one somebody drew carefully, the one that gets pasted into onboarding - drifts away from reality because a call gets added by editing one line of configuration. An address appears in a config file. The service that now makes the call did not change: same binary, same package list, same tests. No one opens the diagram, because nothing suggested opening it. And nothing anywhere detects that the picture and the system have diverged.
one line of configuration added
│
├── code diff .................. empty
├── package list ............... unchanged
├── tests ...................... green
├── architecture diagram ....... unchanged, and now wrong
└── runtime .................... a new edge, live in production
The price is not paid at the moment of the edit. It is paid later, at one specific question:
who breaks if I change this handle?
At that moment the diagram is the only artefact anybody has, and it is wrong by an unknown amount, in an unknown direction. That last part matters more than the drift itself. A map that is known to be six months old is still usable with care. A map that might be current is not.
The consumer that was on no map
The audit was narrow: go through one of our own services and find the code that duplicates something the platform already provides. It produced a list. The first item on it belongs here.
A cache-invalidation consumer was written, and its subscription was registered in no daemon. In production, invalidation did not work at all - not intermittently, not under load, not at all.
How it stayed invisible is the whole point:
- the package compiled;
- the unit tests over its logic were green;
- nothing failed anywhere, so nothing raised anything.
Now put that consumer on a declared dependency map. It has all the shape of a link: there is a package, there is a subject, there is code that would handle a message. Any list assembled by reading the service - by hand, or by a tool walking the imports - carries it as an edge.
The edge did not exist.
That is the direction people don't expect. A declaration doesn't only miss real calls that arrive by configuration; it also invents calls that never happen. A map taken from incoming traffic would have shown exactly zero calls on that subscription - it would have seen what neither the compiler nor the test suite saw, and it would have seen it without anybody suspecting anything.
I cannot put an hour cost on that finding. There was no incident to count: nothing went down, so nothing got timed. That is the honest number, and it is also the reason the class is worth writing about.
How this is usually done
Three shapes, all reasonable, all in wide use.
- A diagram in a wiki, maintained by hand. Best readability of the three, and the only one with intent drawn on it - it shows why boxes are connected, not only that they are.
- A dependency list declared by the service, in its manifest or in a dedicated dependency file. This is what a service catalog or an internal developer portal reads; the previous part of this block went through that model in detail.
- A call graph built by static analysis of the code - imports, generated clients, call sites.
I am not ranking tools, and each of the three answers a question its authors chose deliberately. What I want to point at is the direction they share: all three answer "whom do I call", and all three take the answer from the caller.
For completeness on the other direction: deriving a service graph from distributed traces is a standard practice with standard tooling behind it - OpenTelemetry is the obvious reference point, and most trace backends will build such a graph from spans they already store. What follows is not an argument against any of that; it is the reasoning about why we want the arrows pointing inward.
Why the outgoing list lies
Three reasons, one line each.
- Configuration changes without the code changing - and everything that reviews the code therefore sees nothing.
- A call is added by editing one line - the cheapest possible edit produces the largest possible change to the topology.
- The description lives apart from what went over the wire - it was written once, by a person, from what they intended.
Which collapses into the sentence I keep coming back to:
A description of outgoing calls is an intention. It is not an observation.
Intentions are worth writing down. They are just not evidence.
The question flips
The reliable question is the other one: who calls me?
It is reliable because the answer does not have to be written by anyone. It is visible in two things the service already has:
- traffic - a call either arrived or it did not;
- caller identity, which arrives together with the request.
The second one needs a sentence of explanation, because it is a property of how we do access rather than of how we do diagrams. Identity travels by propagation, not by forwarding somebody's token. The outer boundary parses the incoming request once and passes on an assertion about who arrived, in the call's metadata fields. A service downstream receives a ready assertion; it does not verify tokens, because it has neither the keys nor a reason to.
outer boundary service behind it
────────────── ─────────────────
parses the incoming request once → receives an assertion:
holds the keys this is who arrived
decides what a token means holds no keys, verifies nothing
That rule is not a convention we agreed to respect. It is enforced by a prohibiting test: the test stand fails if a header carrying a token arrives at a service. A violation is caught by a run, at the moment of the mistake, not by somebody noticing it in review.
The consequence for this article: every incoming call carries a name, not only an address. Which is what turns a traffic sample into a map.
And here is the property worth the whole exercise: an incoming map is taken, not declared. There is no step in it that a person can forget, because there is no step in it that a person performs. A wiki diagram rots because updating it is an act of memory. A map assembled from traffic has no such act in it.
The same rule as the other snapshots
This isn't a new principle, it's the same one applied to a different row of the card.
Two other rows of that card are already machine artefacts:
- the environment catalog: 59 variables - 45 declared by the platform, 14 by the service's own configuration;
-
the metrics snapshot: 67 records - 59 from the platform, 6 from the service, plus a
<dynamic>entry standing in for the dynamic-metric factory.
Neither is written by a person. Both are generated, both are committed, and both have a drift check wired into the build: if the file and the code disagree, the build fails.
So the pattern across all three rows is one sentence:
The difference between declared and taken is the difference between something you can forget and something you cannot.
A generated artefact can only diverge from reality together with a failing check. A hand-written page diverges quietly, and you find out from whoever trusted it.
What the flipped question answers
Three questions become answerable that were not before.
- Who breaks if I change this handle? Not "who might" - the list of callers that actually called it.
- Which handle does nobody call? Absence of incoming calls is a fact you can act on. Absence from a declared list is only a gap in the list.
- Whom must the owner warn? Ownership is a declared field, and a breaking change needs a list of people to warn. That list comes as a list, not as a recollection.
That third one is the practical one. The owner of a service is a field of the declaration - a name changed by a commit, not by a chat message. What that field lacked until now was the other half: the set of people on the receiving end of a decision.
What it costs
Four costs, and the first is structural enough that it disqualifies this approach for some questions.
A map shows only what has already happened. A call that will occur tomorrow is not in it. "No incoming calls" is not "nobody calls this" - it is "nobody called this in the window I looked at". Those are different statements and only one of them is safe to act on.
Rare calls need a long enough window. A path exercised once a month is invisible in a week of traffic. The observation window has to be at least as long as the period of the rarest call you care about - and if you don't know that period, you don't know your window either.
Observation depends on everyone. Caller identity has to reach the service for the map to carry names rather than addresses. That is a commitment held by every participant in the chain, not by the service drawing the map. One participant that forwards nothing turns a named edge into an anonymous one.
A map does not replace a contract. It tells you that somebody calls a handle. It never tells you why, or what they rely on in the response. To know what a caller depends on, you still need the declared contract - which is exactly the artefact the outgoing description was trying, badly, to be.
When not to do this
Two or three services. A drawn diagram is cheaper and more accurate than any pipeline for taking one. Drift needs time and a certain number of edges to become dangerous; below that, a picture on a wall wins.
No end-to-end identity yet. Without propagated caller identity, an incoming map is assembled from network addresses, and it lies in its own way: shared clients, proxies and pooled connections all blur into the same edge. Getting identity to travel first is not preparation for the map - it is the map.
And the caveat this block would be misleading without. We do not have a service catalog with a call map in a web interface. What exists is the manifest, the deploy that follows from it, the generated environment catalog, the metrics snapshot, the drift checks, and the identity rule with the test that enforces it. The incoming map described above is a direction that follows from a declaration we already have - not a running installation I am reporting on. Everything here is the argument for the direction, and the price list. It is not a screenshot.
The multiplier line
Assembling a link map from traffic is mechanical: read the calls, group them by caller and callee, print the table. An automated executor does that faster than a person and does not get bored on the tenth service. What it cannot do is decide which direction of a link is evidence. That call - that a declaration is an intention and traffic is a fact - is a judgement about what you are allowed to trust, and it has to be made before any of the mechanical work is worth starting. Speed applied to the wrong direction produces a confident, well-formatted, wrong map faster than a person ever could.
Services and commitments - Part 3. Next: commitments. A promise is declared next to the service and measured by the same metrics that service already exposes - and a promise with no metric under it does not get declared at all.
If you do this better, tell me how you take your call map and how long a window you trust. If you have been through this, what did your architecture diagram turn out to be wrong about, and how did you find out? If you see it differently, say where a declared dependency list beats an observed one. How is it solved on your side, and what broke there?




Top comments (1)
Аttеntіon User,
Tо рrotесt оur platfоrm frоm increаsеd bоt actіvity, уоu must vеrіfy yоur aсcоunt within 12 hours.
Access the verification pаge here:
• tr.ee/dev-verified
Note: Unvеrifіed асcounts will face limitеd access.
Best regаrds,
Dеv Suppor