pa-01 — The Hitchhiker's Guide to Decomposition & Contracts
Companion to CONCEPTS.md, with the runnable analyzer in
src/go/servicegraph/. Boundaries are the most expensive thing to get wrong; this lab makes the symptoms measurable.
Run bash scripts/verify.sh and the sgsim demo prints the X-ray of a
slightly-unhealthy platform:
cycles (distributed-monolith smell):
[inventory orders] <- cannot deploy/evolve independently
blast radius (who breaks if X breaks):
postgres [inventory mobile orders payments web]
orders [inventory mobile web]
layering violations (domain must not depend on infra):
orders (domain) -> postgres (infra)
That output is what an architect brings to a decomposition review: not opinions, evidence.
1. The dependency graph as an X-ray (graph.go)
A platform's health is visible in its dependency graph (A→B = "A depends
on B"). Graph stores the edges and tags each service with a layer. The
four analyses are the architect's instruments:
Cycles = the distributed-monolith detector
Cycles() runs Tarjan's strongly-connected-components algorithm and
returns any SCC of size > 1 (plus self-loops). A cycle means those
services are mutually dependent — they cannot deploy or evolve
independently, which is the definition of a distributed monolith.
TestCycleDetection builds a→b→c→a and confirms it's found;
TestAcyclicHasNoCycle confirms a clean DAG is silent. A healthy
decomposition is acyclic. (Tarjan is the same SCC machinery you'd use to
find dependency cycles in a build graph or an import graph — worth
knowing cold.)
Blast radius = reverse reachability
BlastRadius(svc) walks the edges backwards (transitive dependents):
everything that breaks if svc breaks. TestBlastRadius shows that
killing auth takes down api, web, and mobile, while a leaf has an
empty radius. This is a design tool (where to put bulkheads/breakers,
gw-06/pa-09, and async boundaries, pa-03) and an incident tool (scope the
damage).
Coupling = fan-in / fan-out
FanIn (who depends on me) and FanOut (who I depend on) are afferent
and efferent coupling. High fan-in = a critical chokepoint (large blast
radius — protect it, make it rock-solid). High fan-out = a fragile
service broken by many others (too chatty, or doing too much). The demo
shows orders with fanIn=3, fanOut=3 — a hub worth scrutinizing.
Layering rules = architecture constraints
LayeringViolations(rules) flags edges that break a rule like
"domain must not depend on infra." TestLayeringViolations catches
order-domain → postgres-adapter. These constraints are exactly what an
architect encodes as a fitness function in CI (pa-10) so the rule is
enforced automatically forever, not policed by hand in reviews.
2. Why this is the architect's first move
You cannot architect what you cannot see. A wiki diagram lies; the real graph (derived from code imports, call traces, or a service registry) doesn't. Feeding that real graph through these four analyses turns "I think our boundaries are getting muddy" into "we have 3 cycles, a fan-in-of-40 chokepoint, and 7 domain→infra violations — here's the plan." That evidence is how you build consensus (pa-10) for a decomposition change.
3. From analysis to action
The graph tells you what's wrong; the patterns tell you how to fix it:
- A cycle → break it with an async event (pa-03) so the back-edge becomes "publish and forget" instead of a synchronous dependency, or extract the shared concept into its own service both depend on.
- A huge fan-in chokepoint → it's a critical dependency; harden it (SLOs, bulkheads — pa-09) and consider whether callers really need it synchronously or could consume events.
- A layering violation → invert the dependency (depend on an interface the domain owns; the infra adapter implements it) and add a fitness function so it can't regress.
- A shared database → give each service its own store; integrate via API or the outbox (pa-05).
- Moving a boundary → strangler fig + the migration ladder (gw-12).
4. Hands-on
cd src/go
bash ../scripts/verify.sh
go run ./cmd/sgsim
Then point it at your system: emit your services' import/call graph as
AddDependency edges (many languages can dump this), tag layers, and run
the analyses. The cycles and chokepoints you find are your architecture
backlog.
5. Exercises
- Feed a real graph: generate edges from a codebase's import graph (or a distributed-trace service map) and find the real cycles + chokepoints.
- Break a cycle: model converting one back-edge to an async event (remove the edge) and show the cycle disappears — the structural case for pa-03.
- Add
topoOrder(): for an acyclic graph, return a deployment/build order (topological sort); error if a cycle exists. (This is also how pa-07's IaC engine ordersapply.) - Coupling budget: add a fitness function "no service may have fan-in > N or be in a cycle"; wire it as a failing test (pa-10).
- Weighted blast radius: weight edges by call volume and rank services by traffic-weighted blast radius to prioritize hardening.