3.5 The Physical View
The Development view mapped how the source is organized. The Physical view — Kruchten's deployment view — maps where the built software actually runs: which process lands on which host, across which network boundaries, under what per-host cost and load policy. It is the placement lens, and it earns its keep on a failure the other views cannot see: a system that is functionally correct can still be mis-deployed. Only a view that names where things run catches it. Written down as a placement model the fleet can read, that where becomes something an agent reasons over before it deploys, not a fact discovered after — the Modeling Thesis reaching the one view the others cannot see.
One general type anchors the view, and it is the honest exception in this zoo. A performance and cost model models the computation itself — what it costs to run, what it costs to move data, whether to compute locally or remotely, whether to cache or recompute. The next section takes that gap head-on: the framework supports the cost view, and one real model embodies the load-and-cost half of it, but the pure-latency slice has no dedicated instance, because no failure had justified building one until one arrived. Three real models carry the view: the invariant-DAG execution policy (the idea this chapter turns on — per-host cost and load rationing), the deployment and tier topology model (placement parity, worked in full below), and the substrate-dependency model (a computed migration blast radius).
Learn more about this governance mechanism: deployment and tier topology model.
3.5.1 The performance/cost view: mostly embodied, one honest gap
The zoo names a performance and cost model as a general type, and the framework supports it. But the catalogue holds only a partial instance, and the book would rather state that flat than invent a model to fill the slot.
The load-and-cost half of the view is embodied — by the invariant-DAG execution policy below. Its Scheduler reads a per-host profile of a concurrency ceiling and a budget, and rations both. The more elastic the host, the wider the Scheduler fans work out; the scarcer the box, the harder the plan serializes — and a cost gate is honored everywhere, a scarce-resource gate only where a box demands it. That is a real cost model in action: cost-to-run and resource-contention, driven from a declared per-host profile.
Learn more about this governance mechanism: invariant-DAG execution policy.
What has no dedicated instance is the pure-latency slice: a model of request latency, cache-hit ratios, and data-movement cost. The framework could carry it the way it carries the others — a typed profile, invariants, a drift gate. The zoo names the gap rather than padding it, on the discipline the whole harness leads with: model only where a failure lives. A model built ahead of its failure rots before it is ever checked. But that discipline cuts both ways, and the latency slice is where it drew blood.
DocAble has paid for this gap once. To cut idle cost, the fleet let its services scale to zero — a change the cost half of this model welcomed and the latency half could not weigh. Cold-start latency then surfaced in production, and the surface number hid it: a health check answered in about 150 milliseconds while the PDF-conformance service — which shelled out to a Java validator once per request — carried a warm floor over four seconds, worse on a cold boot. Chasing it cost days of telemetry and a re-architecture that kept the validator resident, cutting that floor from 4,057 to 109 milliseconds. A latency model here — request latency typed per service, the cold-start cost of scaling to zero an invariant over it — would have flagged that penalty before a user ever waited on it. The gap stays honest; this is what it costs to leave open.
3.5.2 The deployment & tier topology model
The typed statement of where things run — each service's name, its layer, and its tier — from which the deploy scripts and layering lints reason about a declared topology rather than scattered constants, and against which the real deploy table is reconciled.
(a) Quality property it helps assess. Two, both a placement fact that drifts silently the moment a service moves.
- Deployment parity: does every service the model declares actually deploy at the tier the model says, and does every deployed service appear in the model? A tier that drifts in the code without a matching model edit fails the build instead of surfacing as a production surprise.
- Layer-boundary soundness: may this layer import that one? The same model carries the layer-boundary graph a cross-layer-import lint holds, so an expedient shortcut across a declared boundary is a finding, not a slow erosion.
(b) Constructs and relations. A frozen record per service, keyed by name.
Service— one deployable unit: its name, its owning layer (web, worker, shared), and its tier class (critical, batch).- The tier relation: each
Servicedeclares the tier it must deploy at; the set of declared tiers is what the deploy table is checked against. - The layer relation: the layers form the dependency graph a cross-layer-import lint reads, and the same topology is the ground truth the migration blast-radius query joins against.
(c) Visual depiction. The natural diagram, Figure 3.5-1, is a deployment diagram — the build host that produces the image, and the runtime cluster it deploys into. Reused from the model's appendix Structure slot:
(d) Invariants, and how they are checked. A bidirectional set-diff and a boundary lint, collected in Table 3.5-1:
| Invariant | Temporal shape | How it is checked |
|---|---|---|
| Every declared service deploys at the tier the model declares | □P (safety) | Parity lint, model ⊆ reality: a declared service missing from the deploy table, or at the wrong tier, is a finding. |
| Every deployed service appears in the model | □P (safety) | Parity lint, reality ⊆ model: a service in the deploy table the model never declared is a finding. |
| No layer imports across a boundary the model forbids | □P (safety) | Cross-layer-import lint over the declared layer graph. |
The parity check is a set-diff run both directions against the live service-to-tier map. Read the model's declared tiers, read the real deploy table, and print each mismatch: a declared service whose tier differs from the deploy table's, and every deployed name absent from the model. Neither direction is optional — model ⊆ reality alone would miss a service that shipped without a model row, and reality ⊆ model alone would miss a tier that drifted in the code.
(e) Traceability and derivation direction. Model-from-code. The parity check re-reads the real deploy table and reconciles the declared topology against it, so the running system is the ground truth for what deploys. The join key is the service name, which indexes both the Service record and its row in the deploy table. The model is authoritative for the layer graph it holds — the import lint reads it directly — and reconciled against reality for the tier placement it checks.
Also seen in: Development (a layer boundary is a packaging fact). Rendered in full here.
That model says where the parts run. The next one says where the checks run — the same physical-placement thinking, applied to the gates themselves rather than the services.
3.5.3 The invariant-DAG execution policy
A deploy graph whose edges carry a typed intent — correctness, cost-gate, or load — kept host-identical, plus a typed Scheduler that reads a per-host profile and rations load and cost so one host fans work out while a scarce one serializes it.
(a) Quality property it helps assess. Two, both about how a host runs the work it was handed.
- Graph portability: is the deploy graph the same on every host? Load-specific edges are banned from the graph and migrated to the Scheduler, so the graph itself carries only host- identical correctness and cost intents, and cannot fork per host.
- Rationing correctness under cost and resource pressure: does the deploy fan work out when it safely can, and serialize only when it must? The Scheduler honors a cost gate everywhere but rations concurrency only where a scarce box demands it, all from one profile table.
(b) Constructs and relations. An edge-intent axis plus a per-host Scheduler.
- The edge intent — each deploy-graph edge carries a typed intent.
CORRECTNESSmeans B is wrong without A, honored on every host.COST_GATEmeans A is a cheap check gating an expensive B, honored by default and relaxable only under an unbounded budget.LOADmeans B contends with A for a scarce box; it is banned in the graph, migrated to the Scheduler. HostLoadProfile— a per-host record: a concurrency ceiling (1 for a single scarce box, large for an elastic one) and a budget (finite to honor cost gates, unbounded to relax them).- The Scheduler — reads a host's profile and emits an execution plan: how many roster items may run at once, and whether to honor the cost gate. Load rationing is a semaphore, not a graph edge.
(c) Visual depiction. The natural diagram, Figure 3.5-2, is a data-flow — edge intents route, load leaves the graph for the Scheduler, and a per-host profile drives the plan. Reused from the model's appendix Structure slot:
(d) Invariants, and how they are checked. A load-edge ban and a plan derivation, collected in Table 3.5-2:
| Invariant | Temporal shape | How it is checked |
|---|---|---|
No deploy edge carries the LOAD intent | □P (safety) | Load-edge lint reads the graph and the Scheduler's graph-resident intent set; a LOAD edge is a finding. |
| The deploy graph is identical on every host | □P (safety) | Graph-parity check: the same graph is emitted for every host; a per-host fork is a finding. |
| The execution plan is a pure function of the host profile | □P (safety) | Derive-and-assert: the plan is recomputed from the profile, never hand-stored. |
(e) Traceability and derivation direction. Model-from-code. The plan is derived from the per-host profile by a pure function, and a hand-stored plan or a LOAD edge is banned outright. The join key is the host name, which indexes both the HostLoadProfile and the deploy phase that runs under the emitted plan. This model is walked in full, with its Scheduler code, in the Scenarios chapter's join example — here it is the Physical-view resident that the join reaches into.
Also seen in: Process (concurrency rationing is a runtime-dynamics concern) and Scenarios (the join example). Rendered in full in the Scenarios chapter; referenced here for the placement and cost half.
3.5.4 The control↔substrate dependency model
The substrate-dependency model makes each governance mechanism declare, as typed metadata, the substrate assumption it bakes in — bound to one substrate, substrate-aware, or substrate-agnostic. It assesses migration safety: before a cross-cutting substrate change, exactly which mechanisms assume the old substrate? That declaration turns a grep into a computed blast radius. Joining the declared stances against the topology model on the migration target prints the in-scope mechanisms as a table — a real answer, not a hopeful search that misses the one whose assumption is implicit. A declaration lint requires the stance, so a mechanism that reads the substrate silently cannot hide from the join. The appendix carries its typed schema and the join code in full.
Learn more about this governance mechanism: control↔substrate dependency model.
---
The Physical view places the parts and rations their cost. That completes the four static views. Next, the +1: the scenarios that set them in motion and validate that the other four hold up.