Overview
A large class of tools does not do the work — it writes the configuration that makes another tool do the work. Scaffolders and project generators, provisioners, IaC wrappers, pipeline generators, dependency and migration managers all share this shape: they take a declaration of intent, emit files a third party reads, and then usually invoke that third party. They also share a recognisable set of failure modes, and the patterns below are the answers.
The unifying idea is that the generated file is the interface, not an implementation detail. Everything else follows from taking that seriously.
Key points
- Render the target's native format. Emit the file the downstream tool already documents, so an IDE, a container build, a CI runner and a colleague who has never heard of your tool all understand the project. A bespoke format that only your tool reads makes every one of those a customer of your tool.
- Orchestrate, never implement. Write the configuration, then invoke the target's own idempotent operation. Reimplementing what the target already does — downloads, checksums, platform matrices — is a maintenance burden that is always slightly wrong.
- Do not route around a solved problem. If the ecosystem already handles something, the correct implementation is to say so and do nothing. An empty step is a legitimate answer, not a stub — and it is more honest than adding a dependency that buys nothing.
- Merge in place when the file belongs to the project. Some target formats live inside files the project owns. Generating those wholesale destroys user content; the rule is to own one field and leave everything else untouched.
- Never produce a partial answer. If a choice cannot satisfy the whole declaration, it must not be offered. A half-configured environment is worse than an unconfigured one: it looks done. Where nothing covers the requirement alone, compose several things that together do, or fail loudly naming the gap.
- Compositions, not copies. A combined answer is a list of members whose behaviour is the union of theirs — render each member's file, run each member's step. The moment a combination is implemented by copying a member's logic, you maintain both forever.
- Make decisions sticky. Record the answer to every question in the project's own manifest, so re-runs follow it without asking again. An interactive tool that cannot be re-run non-interactively is not automatable.
- The record is the source of truth; the generated file is derived. That single direction gives you a consistency check for free: re-render and compare. Disagreement is a report ("out of date"), and writing it back is the fix.
- Re-running must write nothing. Idempotence is not a nice property here, it is the definition of correct: the tool runs on a new laptop, a colleague's clone, a CI runner and after every upgrade, and a re-run that produces churn makes all of those unsafe.
- Resolve derived values in stable-first order. When a declaration must be made more concrete than it is written, prefer what is already on disk while it still satisfies the declaration, and ask an external source only when nothing does. The inverse order makes every run network-dependent and re-derives a perfectly good value the day upstream moves.
- Never invent the missing half. Either reuse a recorded value or ask the component that knows. Guessing produces a value that is plausible, wrong, and hard to trace.
- Single-source every value with more than one consumer, and enforce it with a check that fails when consumers disagree — plus, ideally, one that fails on any value-shaped thing no registry entry claims. The subtle failure is not a wrong value; it is a new surface quietly growing an unregistered one.
Details
The choice-from-coverage pattern
Where a tool must pick between several ways of satisfying a declaration, computing the options beats declaring them:
- Model each candidate by what it covers — the subset of the requirement it can satisfy.
- Given a concrete requirement, offer exactly the candidates covering it whole, plus curated combinations whose union does.
- Record the answer; follow it on later runs.
The payoff is that eligibility rules stop being written down anywhere. A candidate that only handles one ecosystem simply stops appearing once the project needs a second one — no rule, no conditional, no place to forget. The invariant to hold is the one above: a partial cover is never an option, so there is no path to a half-configured result.
The same structure applies well beyond provisioning — anywhere a plugin, backend or strategy is chosen against a set of required capabilities.
Idempotence in practice
Three things make a generator genuinely re-runnable:
- Deterministic rendering — same inputs, byte-identical output. Sort collections, do not stamp timestamps into generated files, do not depend on map iteration order.
- Derived values that stick — see stable-first resolution above; a value re-derived on every run is a file rewritten on every run.
- Delegated steps that are themselves idempotent — which is one more reason to invoke the target's own documented operation rather than a hand-rolled equivalent.
Together these give the steady state its defining property: nothing to write, and often nothing to execute at all.
Generated files people will edit
Users edit generated files. Decide in advance which of three stances applies to each, and say which in the file itself:
| Stance | Meaning | Suits |
|---|---|---|
| Owned | Regenerated wholesale; edits are lost | Files that are purely derived |
| Seeded | Written once, never touched again | Starting points meant to diverge |
| Merged | One region or field maintained; the rest is the user's | Files the project owns |
The unsafe combination is a file that looks seeded and is actually owned. Where an upgrade path would rewrite something a user has customised, the right behaviour is to refuse and name the manual recipe — silently losing their work is the one outcome with no recovery.
Related
- Design patterns — the general catalog; the choice-from-coverage shape is a strategy selection with an explicit invariant.
- Coupling & cohesion — "render the native format" is a coupling decision: depend on the ecosystem's contract, not on yours.
- Infrastructure as code — the same declare-then- reconcile loop, and the drift problem the consistency check answers.
- Version pinning & currency — the single-source registry and its guard, in operational detail.
- Version managers — the worked example most of these patterns were drawn from.
- Architecture documentation — recording which stance a generated file takes is the same instinct as an ADR.