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:
- a top-level document with one line per task
- one detail document per phase, with the full text of every task
- 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 testruns 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