You replaced run-time configuration binding with binders generated at build time and startup improved — what did that cost in artifact size and build machinery?
answer
- the win was paid for somewhere
- constant engine versus per-declaration code
- the build now hosts a step
- incrementality decides the edit loop
- deleting the engine can net smaller
basics
~20 sEmitted code grows with the number of bound declarations, while one inspection engine is a fixed size however many types it handles. The build now hosts a generator that must be incremental, and the code a reader follows is machine-written.
solid answer
~40 sTwo costs land in different places. **Artifact size**: an inspection engine is constant — bind a hundred more declarations and it does not grow — whereas emitted binders grow roughly with declarations times members, so there is a crossover point. The honest caveat is that the total can still fall, because removing the last name-based lookup lets the analyser delete the engine and the type descriptions it needed. **Build machinery**: every build that consumes the tool runs the generator, its output joins the compile path, build time grows with declaration count, and the generator must re-emit selectively or every touch rebuilds everything. There is a real gain hiding in that cost — a class of errors now fails the build — but it is still a cost, paid by every contributor on every build.
go deeper
Remember that moving work into the build does not make it disappear: the build gets longer, and the code that used to be one small engine becomes many small generated pieces.
Contrast the shapes: a fixed-size engine against code that grows per bound declaration, and a build that now runs a generator on every compile rather than none.
Argue the trade with numbers you would actually gather: startup time attributable to binding, artifact delta after shrinking, and the effect on the edit-build loop of the people who work in this tree.
Weigh a recurring cost imposed on every consuming team's build against a saving on every invocation, and decide who is entitled to make that choice for whom.
## What the generator adds to the artifact The two strategies have opposite size profiles: - **Inspection is constant.** One engine walks any type's description. Binding one declaration or four hundred adds no code, though the runtime must carry enough description of each type for the engine to read. - **Generation is linear.** Roughly one assignment per bound member, plus a binder per bound declaration, plus whatever registration wires them together. Four hundred declarations means four hundred binders. So there is a crossover, and where it sits depends on how much of the object graph is bound. A tool with a handful of option objects will never notice; a tool that binds a large model can find the emitted binders are a visible share of the shipped artifact. The honest complication, and the one that makes a flat "generation is bigger" answer wrong: once **no** code resolves members by name, the inspection engine becomes unreachable, and so do the descriptions kept only so the engine could read them. A shrinker can then delete all of it. Whether the total rises or falls is an empirical question about the ratio between what the engine and its descriptions weighed and what the emitted binders weigh — not something to assert from the armchair. ## What the generator adds to the build This is where the cost is certain rather than conditional: - Every build that consumes the tool must run the generator, including a contributor's laptop build and every build-farm job. - Emitted sources join the compile path, so there is more to compile, and compilation of the emitted code is proportional to the declarations bound. - The generator must be **incremental**. If touching one file re-emits and recompiles every binder, the edit-build loop degrades for everyone, and the degradation is worst on the machine with the least power. - The generator becomes a dependency with a version. Upgrading it changes emitted code across the whole tree at once. - A reader following the code lands in machine-written source that no one on the team wrote, which is a navigation cost even when the emitted code is perfectly clear. ## The gain hiding inside the cost It is not all loss, and a senior answer says so. Because the generator runs while the build runs, a class of defects moves from a run to a compile: a member whose declared type has no conversion, a duplicate key, a key that names nothing. Moving a failure earlier is worth real build minutes. The trade is best stated as *when the failure happens* against *who pays for the build*, rather than as fast against slow. ## The two sides, side by side | | Emitted binders | Run-time inspection | |---|---|---| | Code size | grows per bound declaration | one engine, fixed size | | Type descriptions in the artifact | can become deletable | must be retained | | Build time | grows with declarations; needs incrementality | unchanged | | What a contributor installs | the generator, on every build | nothing | | First failure for a bad member | the build | the run that binds it | | What a reader follows | machine-written source | one engine, plus data | ## When the trade goes the wrong way 1. **Binding was never in the startup budget.** If the measurement says binding costs a fraction of a millisecond, the build cost buys nothing. 2. **The declaration count is very large.** Emitted code and emitted-code compile time both scale with it, and the startup saving does not scale the same way. 3. **Many teams consume the library.** One generator is one build step for you and N build steps for them, and they did not choose it. 4. **The process is long-lived.** A service amortises discovery over its whole run, so the startup argument that justifies the generator does not apply. 5. **Some bound types arrive after the build.** Then generation cannot cover them anyway, and you have paid the build cost for partial coverage. ## How it is asked The question usually comes as a follow-up to a startup win: *good — what did it cost?* A weak answer stops at "a bit more build time". A strong one separates a size effect that is linear and might still net out smaller, from a build effect that is certain, recurring, incremental-or-painful, and paid by everyone who compiles the code — and then names the class of errors that moved into the build as partial payment.
- Why might the shipped artifact end up smaller after generation rather than larger?Because generation can remove the last reason to keep the inspection engine and the type descriptions it reads. Once nothing resolves members by name, the analysis can delete both. Whether that outweighs the emitted binders depends on their relative weight, so it is a measurement, not a rule.
- What makes an added build step painful in practice rather than merely slower?Loss of incrementality. If the generator re-emits everything whenever anything changes, every edit recompiles every binder, and the cost lands on each contributor's edit-build loop rather than once in a pipeline. A generator that tracks which declarations changed and re-emits only those keeps the loop intact.
saying these in an interview costs you the question
- States flatly that generated binders always enlarge the shipped artifact
- Ignores that the build step is paid by every consuming build, not once
- Assumes an added generator is automatically incremental
- Compares only startup time and never the artifact or the build
- Forgets that moving a failure into the build is part of the payment