skip to content

A command-line tool binds configuration onto objects by generated code or run-time inspection: which work does each leave for startup?

level: middleimportance: must knowfreq 62%

answer

  1. same plan, two different phases
  2. plan versus assignment
  3. discovery is the part that moves
  4. the build decides it once, or the process rediscovers
  5. emitted binder is straight-line assignment

basics

~20 s

Generation decides the binding plan during the build, so startup only runs straight-line assignments. Run-time inspection computes the same plan inside the running process, so member discovery, key matching and conversion choice all land in the startup window.

solid answer

~40 s

Binding a configuration value onto an object is two separate things: a **plan** — which key feeds which member, and how raw text becomes that member's declared type — and the **assignment** itself. The plan depends only on the declaration, so it can be decided while the build runs and emitted as ordinary code; startup then performs assignments and nothing else. Run-time inspection computes that same plan from the type's own description inside the process, so the discovery happens in the startup window on every invocation. The assignment is comparable either way; what moves between phases is the discovery. Inspection buys something real in return: it binds types the build never saw, and it puts no generator into anybody's build.

code

pseudocode · 13 lines
pseudocode
// plan computed in the running process
function bindByInspection(target, values)
    for each member in describeMembers(typeOf(target))
        if values.has(member.name)
            raw = values.get(member.name)
            setMember(target, member, convert(raw, member.declaredType))

// same plan, emitted while the build ran
function bindServerOptions(target, values)
    if values.has("port")
        target.port = toInteger(values.get("port"))
    if values.has("verbose")
        target.verbose = toBoolean(values.get("verbose"))

go deeper

for a junior

Recall that the same binding job can be worked out while the program is built or worked out while it runs, and that the second choice repeats that work every single time the tool starts.

for a middle

Separate the plan — member list, key match, conversion choice — from the assignment, and say which phase each strategy computes the plan in. That split is the whole answer.

for a senior

Show how you would decide for a real tool: measure what binding costs inside the startup window, check whether every bound declaration exists while the build runs, and name what the generator adds to every consuming build.

for a principal

Frame it as where the organisation chooses to pay: build minutes and a generator each consuming build must host, against startup milliseconds charged on every invocation by every user.

## The job binding actually has to do Whatever strategy a tool picks, turning configuration values into state on an object is the same four steps: 1. **Discover** which members the target type exposes, and what each member's declared type is. 2. **Match** each configuration key to one of those members. 3. **Choose a conversion** from the raw textual value to that member's declared type. 4. **Assign** the converted value, past whatever visibility rule guards the member. Steps 1 to 3 are a **plan**, and the plan depends only on the shape of the declaration — which is fully known while the build runs. Step 4 is the only step that genuinely needs a value in hand. The whole emit-or-inspect argument is about which phase computes the plan, not about whether the plan exists. ## Emitting the plan during the build A generator reads the declarations as part of the build and writes ordinary code that does step 4 and nothing else. What that buys: - The emitted code contains one assignment per bound member, with the key and the conversion already chosen. - The compiler type-checks the result like any other code, so a member whose declared type has no conversion can fail the build instead of a run. - At startup the tool reads values, converts them and assigns. There is no member list to walk and no name to match. What it demands is equally concrete: the declaration must exist while the build runs, and every build that consumes the tool must host the generator. ## Computing the plan in the running process Inspection asks the loaded type to describe itself, then derives the same plan in memory. That work is not free and it is not optional — it happens on the path that binds, which for a short-lived tool is the startup path. In exchange: - It binds types that did not exist when the tool was built, including ones loaded from an extension directory. - It needs no build step, no generated sources on the compile path and no extra tool for a contributor to install. - One small engine handles every type, so adding a hundred more bound declarations adds no code. ## Where each unit of work lands | Work | Decided by a generator | Decided by inspection | |---|---|---| | Member discovery | while the build runs | in the process, when binding runs | | Key-to-member match | fixed in the emitted code | recomputed from the type's description | | Conversion choice | resolved against the declared type during the build | resolved from the description while running | | The assignment | straight-line code | performed through a member descriptor | | An unbindable member | can fail the build | fails when that path first binds | | A type the build never saw | not covered | covered | ## Why a short-lived tool makes this the argument A process that runs for hours pays discovery once and then forgets about it: whatever the plan cost, it is spread over the whole run. A command-line tool that lives for a few hundred milliseconds has no such run to spread it over — its entire lifetime *is* the startup window, and it pays again on every invocation, for every user, in every script that calls it in a loop. That is why the same library will reasonably inspect inside a long-running service and generate for a tool, and why the interviewer wants to hear the phase argument rather than a preference. The magnitude is worth being honest about. The discovery is per type and per member, and it scales with how much of the object graph is bound, not with how many values the user actually supplied. A tool that binds three members will not notice; one that binds a deep tree of nested option objects is doing real work before it prints anything. ## What this choice does not decide - It does not make binding free. The emitted binder still runs at startup — it just runs a plan instead of making one. - It does not by itself settle how expensive an individual assignment is once the plan exists; that is a separate measurement question. - It does not settle what the shipped artifact ends up weighing, or what the build has to host. Both change, and both push the other way. Some ecosystems lean hard on generation for exactly this reason and others lean on inspection; where they differ, the difference is usually about how much build machinery their users tolerate rather than about the mechanism. ## How it is asked The interviewer's version is short: *why would a library generate a binder instead of just looking at the type?* A weak answer says "reflection is slow". A good answer separates plan from assignment, names the phase each strategy computes the plan in, and finishes with the cost on the other side of the trade — the build step, and the types a generator cannot see.

  • Why can a tool that loads extensions after it starts not generate all of its binders?
    A generator only sees declarations that exist while the build runs. An extension type that arrives afterwards was never in that input, so either something inspects it in the process, or the extension's own build emits a binder and registers it. Generation is not a property you can retrofit onto a type nobody compiled.
  • Does emitting the binder remove the startup work entirely?
    No. The emitted binder still runs at startup: it reads values, converts them and assigns them. What disappears is *deciding* what to assign — the member list, the key match and the conversion choice are already fixed in the emitted code.
  • If binding measures at a millisecond, is the generator still worth it?
    Often not. The honest order is to measure the startup window first, then decide; the discovery scales with how much of the object graph is bound, so a tool with three options and one with a deep nested option tree are different cases. If it is genuinely negligible, inspection costs the build nothing and keeps extension types working.

Writing the packing list the night before, or working out what to pack while the taxi waits. The list is identical; only one version of it happens inside the time you are being measured on.

saying these in an interview costs you the question

  • Thinks the only difference is per-assignment speed, not the discovery that is skipped
  • Says generation removes startup work entirely, when the emitted binder still runs
  • Cannot name a single reason anyone would still inspect types while running
  • Believes the emitted binder decides key-to-member matching while the tool runs
  • Treats the choice as a style preference rather than a decision about phase