The build · 6 min read

Planning the build

How did the approved design become five phases of work, one pull request each and tests first? And which choices did the design leave for the plan?

Roar Elias Georgsen, 27 August 2026

Part 9 of 13 in Federating a systems model, written for release v0.3.0.

The story so far

The demo is a SysML v2 model of a five-server query pipeline, served as one subgraph of a federated GraphQL graph. Beside it sit a capacity service that finds the pipeline’s bottleneck and a document service that owns a requirements document, all in one container. The design phase is over. This part is about the plan that turned the design into code, and the things the plan had to decide for itself.

The design ended with four gates approved, twenty-six decision records and an architecture description that names every type, field and process. It had no sequence of tasks. The plan had to be detailed enough to build from without deciding anything again, and it opens on the finish line:

Build the pipeline demo the design phase specified: a generic SysML v2 adapter subgraph, a capacity service, a document service, two vanilla web apps and a Cosmo router, shipped as one container image launched by one docker run

The decision records had already fixed the stack:

Part Choice
Language Go 1.27
The three services gqlgen 0.17.94, with federation and WebSocket subscriptions
Router Cosmo router 0.343.1, run as a child process
Composition wgc 0.130.1, run by hand, with its output committed
Web apps plain ES modules and one vendored file, SortableJS (no build step)
Image a distroless base, published on a version tag

The plan came in three layers:

  1. a top-level document with one line per task
  2. one detail document per phase, with the full text of every task
  3. a map of every requirement against the task that meets it and the test or demonstration that proves it

With the top-level document and the full text of whichever task I was on, I had enough to work from.

Five phases, one pull request each

Before the five phases of building came a phase zero, with its own pull request like the rest.

Phase What it builds
0 repository policy, and the spikes that settle what the documentation couldn’t
1 the example model and the adapter core
2 the three services, composed into one graph
3 the supervisor: one binary, one process tree, one port
4 the two web apps
5 the image, and the first release

Phase 0 runs the spikes. Each is a small experiment that answers a question the architecture couldn’t settle from reading, with a pass criterion and a fallback written down first. Four of the five run there. The fifth, a run with no network, needs the built image and waits for Phase 5. Five spikes before the first line has what they found.

Every phase is one pull request. Tests come first, with the test as the specification, and a test that verifies a numbered requirement is named for that requirement. make check runs after every task, and a coverage floor of 70 per cent is checked before every push. Requirements about browser behaviour get checklists, run by hand and recorded with the date and host.

Update Changed in v0.4.0

Each acceptance criterion now also has a Gherkin scenario, a plain-text specification in given, when and then that a reader can follow without reading Go. go test runs every scenario that can run there. The tests named after requirements stay, and none was renamed (acceptance criteria as Gherkin scenarios).

The end-to-end proof is the worked example from From use cases to requirements, observed through the router and both web apps. It runs from the shipped state failing at parse, through the bottleneck moving and the pipeline passing while one server fails its share, to reset putting everything back.

Why the line figures are estimates

One of the design’s constraints is that the repository can be read in an afternoon, which means little unless somebody counts. So the plan carried line figures. The first estimate for the adapter was 2000 lines of hand-written Go. It lasted exactly as long as it took to write the adapter’s tasks out in full:

Adapter package First estimate In the written-out tasks Revised
syntax 800 about 1261 1300
model 650 about 932 1000
projection and serve 550 about 389 450
Total 2000 about 2582 2750

The other components got estimates too: 600 lines for each example service, 700 for the supervisor, and 900 lines of JavaScript and 300 of CSS for each web app.

The figure needed revising the moment real text existed, and that settled what kind of number it is (the line figures are estimates). Correctness comes first, and a component that needs more lines to be correct takes them. The counts are still measured at each phase’s close, because the scale is worth seeing. Nothing is trimmed to fit, though.

Under a figure written as a limit, the first lines to go are the cheapest to give up. Those are the doc comments, the error messages that say where the problem is, and the guards that make a refusal honest. They’re exactly the parts a reader with an afternoon most needs.

What the plan decided for itself

A plan detailed enough to build from has to decide things the design didn’t, and say so. Most of those decisions were small. Two are worth telling.

The model in memory is immutable once built. An edit returns a new model with the version bumped, and the adapter’s store swaps one pointer. So a query can never see the served text and the projection disagree, even with many readers at once.

The editable numbers moved out of the text. The viewer decision had put inline inputs at each number’s position in the model text. But the projection carries values without their source positions. Placing an input at a number’s position would have meant the browser searching the text for the number. That’s a second and weaker parser of a language the adapter has already parsed. The plan put the editable numbers in a panel above the text instead. The served text still shows each edited number where it was, because the adapter patches it there. I still think the panel suits a demo whose point is the model text, and the decision record was amended to match.

Two points stayed open into the build. The curated projection decision leaves open an escape hatch for elements nobody has projected yet. That hatch is still unbuilt, so a systems model using a construct outside the subset doesn’t load. And inline inputs would need source positions in the adapter’s schema, which nobody has decided are worth adding.


Previous: An A3 sheet for a fifteen-minute reader · Index: Federating a systems model · Next: Five spikes before the first line