The Software GardenDerivation over duplication ← Contents
The Software Garden

Derivation over duplication

Every fact in a system should be written down once.
Everything else that needs it should be generated from that one place, and checked.

Scroll ↓
Why

Hand-kept copies drift. Agents make them drift faster.

When the same fact lives in four places, every change has to find all four. An agent working from its context window finds the ones it can see.

The result is the whack-a-mole from the zero-eyes chapter, but for facts: types that disagree with the API, docs that disagree with the code, examples that no longer compile.

Derivation removes the copies. There's one place to change, and the rest follows.

Hand-kept copiesDerived
API spec: ProcessInstance { key, state, tenantId }
TypeScript typesProcessInstance.ts
Python modelsprocess_instance.py
README exampleREADME.md
CLI helpc8ctl help get pi
 

Illustrative change.

At scale

One spec. Seven SDKs. No hand-written API surface.

The Camunda Orchestration Cluster SDKs (TypeScript, Python, C#, Go, Java, Rust, PHP) are generated from one OpenAPI spec in the camunda/camunda monorepo.

A shared bundler fetches and normalises the spec. Each SDK's pipeline generates the models, the client, the facade and the semantic key types: JobKey and ProcessInstanceKey are distinct types in every language, because the spec says so.

The hand-written part is the runtime: auth, retry, backpressure, workers. That's where the judgement lives.

OpenAPI speccamunda/camunda
↓
camunda-schema-bundlerfetch · bundle · normalise
↓
TS
Python
C#
Go
Java
Rust
PHP
↓
models
client
facade
semantic keys
↓
c8ctlconsumes the TS SDK
The one rule

Never edit the output. Fix the generator.

Every SDK marks its generated directories as never hand-edit: src/gen/, client/, generated/. When the output is wrong, the fix goes in a numbered post-processing hook, so the pipeline stays reproducible.

A hand edit is the most tempting fix for an agent: it's local, it's quick, and the test passes. And the next regeneration silently wipes it out.

✏️ Hand-edit the output

 

🪝 Fix it in a hook

 
 

Illustrative file names.

Not just code

Docs, help and examples are derived too.

Documentation is the copy that drifts most, because nothing executes it. So we make sure something does.

README code blocks are injected from compiled example files. If the API changes, the example stops compiling, long before a user copies broken code.

examples/readme.tstype-checked
→
README code blocksall seven SDKs
examples/operation-map.jsonop → example region
→
@example in API docsand IntelliSense
ConfigResolver::configReference()PHP SDK
→
README config referenceevery env var
COMMAND_REGISTRYc8ctl
→
help · completions · arg parsing · validation“no metadata is duplicated anywhere”
Trust, but regenerate

A derived file is only derived if CI proves it.

Otherwise “generated” is just a comment at the top of the file. Each gate regenerates and compares: sync-readme-snippets --check, make config-reference-check, make example-coverage.

Upstream drift is handled differently. The Go SDK re-bundles the spec nightly. When the spec has moved, the drift job is report-only: it uploads a patch for a follow-up regeneration PR instead of failing, so routine upstream churn doesn't hide a real regression.

Reviewability

Change the generator in one commit. Regenerate in the next.

Generator changes are small and meaningful. Regenerated output is large and mechanical. Mix them, and nobody (human or agent) can review the change.

Keeping them apart also keeps backports clean (the target branch regenerates its own output) and keeps git blame useful on both sides.

fix: handle nullable tenant ids
+4,812 −3,906
Twelve meaningful lines, somewhere in 8,700. Good luck.
fix: handle nullable tenant ids in key hook
+9 −3
The actual change. Easy to review, easy to cherry-pick.
chore(gen): regenerate for nullable tenant ids
+4,803 −3,903
Mechanical. Verified by regenerating, not by reading.

Illustrative commits; the convention is from the SDKs' AGENTS.md.

So

If an agent could update it in one place and forget another, derive it.

Write each fact once. Generate types, clients, docs, help and examples from it. Fix the generator, never the output. Let CI prove the derived files match, and keep generator changes reviewable.