skip to content

Metadata Consumers

The consumers of attached metadata: build-time processors, startup scanners, and call-site checks, plus scan caching and silent no-ops. Interviewers ask because that is where framework magic lives.

on this pageshow

questions

4

A marker attached to a class declaration changes nothing on its own — what must exist for it to have any effect?

level: juniorimportance: must knowfreq 60%

answer

  1. declaring is not the same as doing
  2. someone has to look for it
  3. a consumer, running at some phase
  4. no consumer, no error, no behaviour
  5. enumerate, read, then act

basics

~20 s

Attached metadata is inert: something has to read it. A consumer — a build-time processor, a startup scanner, or a check at the call site — must look for that marker and act on what it says, or nothing happens.

solid answer

~40 s

Attaching a marker to a declaration only records a fact next to that declaration; it compiles away into the artifact and changes no control flow. The behaviour everyone attributes to the marker actually lives in a separate piece of code, the consumer, which enumerates some set of declarations, asks each one whether that marker is attached, reads its member values, and then does something — registers the component, validates it, wraps it, or rejects it. Three things must all hold: the marker must still be readable at the phase the consumer runs in, the consumer must exist, and its candidate set must actually include the declaration you marked. If any of the three is missing, the program behaves exactly as if the marker were not there, and nothing reports a problem.

code

pseudocode · 15 lines
pseudocode
// attaching the marker: purely descriptive, no behaviour
marker Scheduled(everySeconds)

Scheduled(everySeconds = 30)
class ReportRefresher:
    function run():
        refreshReports()

// the consumer. Without this running, nothing above ever fires.
function startScheduler(typesInDeploymentUnit):
    for each type in typesInDeploymentUnit:
        m = markerOn(type, "Scheduled")
        if m == none:
            continue                 // unmarked types are skipped, silently
        repeatEvery(m.everySeconds, () -> newInstance(type).run())

go deeper

for a junior

Remember the one-line version: metadata describes, code decides. If you attach a marker and nothing changes, that is the expected result until some consumer reads it.

for a middle

Be able to name the three steps a consumer takes — enumerate candidates, read the marker, act — and explain that the meaning of a marker lives entirely in the consumer, never in the marker itself.

for a senior

Show the diagnosis order in a real deployment: confirm a consumer for that marker exists there, confirm it ran, then confirm its candidate set included the declaration. The declaration is rarely the fault.

for a principal

The trade you are actually approving is decoupling paid for in silence. Decide whether your platform accepts a marker whose consumer may be absent, or requires every marker to have an owning consumer that can be checked.

## Attaching and reading are two separate acts Attached metadata — a **marker** written next to a declaration, sometimes carrying member values such as a name, a number or a flag — is a *description*, not an instruction. Attaching it records a fact about that declaration somewhere another program can find it later. The declaration compiles as it did before, its control flow is unchanged, and no code runs merely because the marker is present. Everything people casually call "what the marker does" lives in a second, entirely separate piece of code: the **consumer** that goes looking for that marker and acts when it finds it. That split is the whole mechanism. It is what lets a marker be added to a codebase that has no idea what it means without breaking anything — and it is also why a marker can sit on fifty declarations for a year doing nothing at all. ## What a consumer has to do Wherever it runs, a consumer performs the same three steps: 1. **Enumerate candidates.** It picks the set of declarations it is willing to look at: everything this build compiles, everything reachable in the deployment unit when the process starts, or just the single target of the call being made right now. 2. **Read the marker.** For each candidate it asks whether that specific marker is attached, and reads the member values if it is. 3. **Act.** It registers the component, validates it, wraps it, records it in a list, or refuses to continue. **The meaning lives here** — in the consumer's code — and nowhere else. Drop any one of the three and the marker is inert. The most common real-world drop is the first: a consumer exists and works perfectly, but the declaration you marked was never in its candidate set. ## Where a consumer can run | Consumer | Runs | Typical act | |---|---|---| | Build-time processor | while the build compiles the declaration | check the marker is well placed, record it, fail the build | | Startup scanner | once, when the process boots and assembles itself | register each marked component it finds | | Call-site check | on each call through a marked member | decide what this particular invocation should do | One precondition sits under all three: a consumer can only read a marker that still exists at the phase it runs in. How long a marker survives — discarded after parsing, kept in the artifact, or queryable while the program runs — is its own subject. The point here is simply that surviving is *necessary but not sufficient*: something still has to look. ## Why nothing complains A missing consumer is silent, and the silence is structural rather than an oversight: - The marked declaration is **well-formed on its own**. Nothing about it is incomplete without a consumer, so no build step and no loader has grounds to object. - **Unrecognised markers are normal.** Several unrelated consumers commonly read different markers on the same codebase, so "a marker nobody here recognises" cannot be treated as an error without producing constant false alarms. - **Nobody owns the question.** There is no component whose job is to ask "should something have read this?", because answering it would require knowing every consumer that might ever exist. - The observable result of *no consumer* is identical to the observable result of *feature switched off*: the plain, unmodified behaviour of the code. ## What the inertness buys The same property that makes the failure silent is what makes the technique useful: - A library can ship marked declarations that are harmless to anyone who has no consumer for them — they cost some space in the artifact and nothing else. - A team can mark declarations before the tooling that reads them is written, or keep them marked after it is removed. - Two independent consumers can read the same declaration for different purposes without coordinating, because neither one changes the declaration. The matching hazard is that two consumers can also read the *same* marker and interpret it differently. The marker itself carries no semantics to arbitrate between them. ## Debugging from the right end Because "nothing happened" is the expected symptom of a missing consumer, diagnosis starts at the consumer and works backwards: does a consumer for this marker exist in this deployment, does it run at all, and does the set of declarations it enumerates include the one you marked? Staring at the marked declaration itself will almost never reveal the fault, since the declaration is usually exactly right.

  • If two independent consumers read the same marker, what decides what it means?
    Each consumer does, separately. The marker carries member values but no semantics, so one consumer may treat it as a registration hint and another as a validation rule, and neither can see the other's interpretation. That is a genuine hazard: the fix is ownership — one team owns the marker and the consumer that defines it, and other readers of it are documented.
  • Why can a library ship marked declarations that are harmless to a user with no consumer for them?
    Because the marker changes nothing about how the code compiles or runs. A user without a consumer gets the plain behaviour of the declarations, and pays only the space the metadata occupies in the artifact. That is what makes optional, opt-in tooling possible without forking the library.
  • A consumer exists and runs, but one marked class is still ignored. Where do you look?
    At the consumer's candidate set. A consumer only sees declarations it enumerates, and that set is a deliberate choice — one part of the deployment unit, one kind of declaration, one naming convention. A class outside that set is invisible no matter how correctly it is marked.

A coloured sticker on a parcel only routes it if someone at the sorting belt has been trained to watch for that colour. The sticker moves nothing by itself, and an unwatched colour is not an error — the parcel simply travels the default route.

saying these in an interview costs you the question

  • Thinks attaching a marker itself causes code to run
  • Assumes any marker is automatically discovered by the runtime
  • Says a marker with no consumer is a compile error
  • Believes the marker's member values are executed rather than read
  • Cannot say who reads the marker or at what phase
  • Blames the marked declaration when nothing happens, never the consumer
open as a page

A marker can be consumed by a build-time processor, a startup scanner, or a call-site check — when does each catch a mistake?

level: middleimportance: must knowfreq 66%

basics

~20 s

A 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.

open as a page

A service scans every class in its deployment unit at startup to find marked components — what does a build-time index of those markers change?

level: seniorimportance: should knowfreq 46%

basics

~20 s

It moves the search off the startup path. The build records which declarations carry the marker, and the booting process reads that list instead of inspecting every class. Startup gets cheaper, and a stale index becomes a new failure mode.

open as a page

A marked component is silently ignored in production — how would you make a consumer's failure to see that marker detectable?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

Turn absence into a signal. Have the consumer publish the inventory it accepted, assert against a known marked fixture in a test, fail the build on a marker no consumer claims, and fail closed where silence is dangerous.

open as a page