A marker can be consumed by a build-time processor, a startup scanner, or a call-site check — when does each catch a mistake?
answer
- three moments, not one
- build, boot, or call
- earlier catch, narrower view
- the scanner sees the assembled unit
- a call-site check waits for that path
basics
~20 sA build-time processor catches a mistake while the build runs and can fail it. A startup scanner catches it when the process boots, after the artifact already shipped. A call-site check catches it only when that path actually executes.
solid answer
~50 sThe three consumers differ in one axis that matters most — how early feedback arrives — and in a second that is easy to miss: what each one can see. The build-time processor runs earliest and can refuse to finish the build, but it only sees what that build compiles, so it cannot know what other artifacts will be assembled beside it. The startup scanner runs once when the process boots and sees the whole assembled deployment unit, so it can catch conflicts and gaps that span artifacts — but the cost of a mistake is now a failed or misconfigured deployment. The call-site check runs last, sees the real receiver and arguments, and therefore catches things neither of the others can — but only on paths that are actually executed, so a wrong marker on a rare path can hide for months.
code
pseudocode · 18 lines// 1. build-time consumer: runs while compiling, can refuse to finish
onBuild(declarationsInThisBuild):
for each d in declarationsInThisBuild:
if hasMarker(d, "Cached") and d.returnsNothing:
failBuild(d, "Cached on a member that returns no value")
// 2. startup consumer: runs once, over everything assembled into the unit
onStartup(typesInDeploymentUnit):
for each t in typesInDeploymentUnit:
if hasMarker(t, "Cached"):
register(t) // conflicts across artifacts show up here
// 3. call-site consumer: runs per invocation, only on paths taken
function invoke(target, args):
m = markerOn(target, "Cached")
if m == none:
return target(args)
return cachedResultOr(m.region, target, args)go deeper
Learn the three moments in order — build, boot, call — and the one-line consequence: the earlier the marker is read, the cheaper the mistake is to fix.
Explain both axes, not just timing. Each consumer sees a different slice: one build's declarations, the whole assembled unit, or a single live invocation, and that slice decides what it can possibly catch.
Show the judgment in a real system: which checks you pushed into the build, what you deliberately left to boot because it only exists once artifacts are combined, and how you stopped a rare path being the first to find an error.
Frame it as where failure should land for the organisation — a red build for one team, a blocked rollout for a service, or a run-time error in front of a user — and set the standard accordingly.
## Three moments, one marker A marker attached to a declaration is read by whatever code goes looking for it, and that code can run at three distinct moments in the life of the software: 1. **While the build compiles the declaration.** A build-time consumer is handed the declarations being compiled and inspects the markers on them. Its distinguishing power is that it can *stop the build*, so a mistake never reaches an artifact at all. 2. **When the process boots.** A startup consumer walks the deployment unit that was assembled from many artifacts, finds the marked declarations, and registers or configures them. Its distinguishing power is that it sees the *whole assembled system* rather than one build's worth of code. 3. **On the call itself.** A call-site consumer reads the marker on the member being invoked, right now, with the actual receiver and arguments in hand. Its distinguishing power is that it sees *run-time state* that no earlier phase could know. The same marker often has consumers at more than one of these moments — validated during the build, registered at boot, honoured per call — and that combination is usually the strongest design rather than a redundancy. ## When each catches a mistake | Consumer | Feedback arrives | What it can see | Mistake it cannot catch | |---|---|---|---| | Build-time processor | before an artifact exists | the declarations this build compiles | anything about how other artifacts will be combined with it | | Startup scanner | on every process start | every marked declaration inside the assembled deployment unit it enumerates | anything decided per invocation, and anything outside its candidate set | | Call-site check | when that path executes | the marker plus the real receiver, arguments and configuration | anything on a path nobody exercises | Read the table as a progression: **feedback gets later, and the information available gets richer.** That is the trade, and it is the reason teams use more than one. ## What "earliest" actually buys Failing the build is the cheapest failure in the list. Nothing was published, no environment changed, and the person who made the mistake is still looking at the code. A build-time consumer is therefore the right home for anything that is decidable from the declaration alone: - the marker is attached to a kind of declaration it may not target; - two member values contradict each other; - a required partner marker is absent; - the marked member's shape cannot possibly work with what the marker promises. The limit is real, though: the processor only sees what *that build* compiles. A marker on a declaration compiled elsewhere, or a conflict that only appears when two independently built artifacts are combined, is invisible to it. ## What the startup scan sees that the build cannot By the time a process boots, the deployment unit has been assembled from everything the deployment actually contains. That assembled view is the only one in which certain mistakes exist at all: - two marked components claiming the same registered name or role; - a marked component whose declared partner exists in no artifact present; - a required component missing entirely from this deployment, though every individual build was clean. The price is the moment: this failure happens during a deployment rather than a build, and every instance start pays the scan again. A startup consumer also only enumerates the candidates it was pointed at, so "it scans everything" is a comforting fiction — declarations outside that candidate set are silently not examined. ## What only the call site knows The last consumer reads the marker with the invocation in front of it, which is why it can act on facts that simply do not exist earlier: which concrete target is actually being called, what the arguments are, what the current configuration says, which tenant or user this call belongs to. That is genuine capability, not just lateness. The corresponding weakness is coverage. A call-site check validates only what is invoked, so a marker that is wrong on a rarely-taken path is not caught, and a marker on a path never taken in production is never examined at all. "It works in production" from a call-site consumer means "the exercised paths work", nothing more. ## Choosing where to consume A useful default: - **Decidable from the declaration?** Put it in the build, so the failure is cheapest. - **Only decidable once everything is assembled?** Put it at startup, and make the failure loud there rather than deferring it. - **Only decidable with the call in hand?** It belongs at the call site — and then pair it with an earlier check that at least confirms the marker was well placed, so the rare path is not the first thing to discover the error.
- Why can a build-time consumer reject a marker that a startup consumer cannot?Because it can stop the build, so the mistake never becomes an artifact and never reaches an environment. A startup consumer can only refuse to start something already built and deployed, which means the same rejection now costs a failed rollout and a rollback rather than a red build.
- What can a call-site consumer catch that neither of the others can?Anything that depends on the invocation: the concrete target actually resolved, the argument values, the configuration in effect for this call. Those facts do not exist during the build or at boot, so no earlier consumer can decide against them — at the cost of only ever examining paths that run.
- Is it wasteful for the same marker to be consumed at two phases?Usually not. Validating placement during the build and acting on the marker at startup or per call are different jobs, and the earlier one exists precisely so the later one never meets a malformed marker. The duplication to avoid is two consumers making the same decision differently.
saying these in an interview costs you the question
- Thinks all marker consumption necessarily happens at run time
- Assumes a build-time processor sees the whole deployed system
- Says a startup scan examines every declaration that exists
- Treats a call-site check as equivalent to build-time validation
- Cannot name a mistake a startup scan catches that a build cannot
- Believes later consumption is strictly worse than earlier