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

  1. Feed a real graph: generate edges from a codebase's import graph (or a distributed-trace service map) and find the real cycles + chokepoints.
  2. 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.
  3. 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 orders apply.)
  4. 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).
  5. Weighted blast radius: weight edges by call volume and rank services by traffic-weighted blast radius to prioritize hardening.