skip to content

How would you govern a shared framework's renderer registry so one team's registration cannot change another team's routes?

level: principalimportance: nice to knowfreq 29%

answer

  1. global registry, local routes
  2. opt-in beats inherit
  3. explicit priority, not file order
  4. strictness per route class
  5. add representations additively

basics

~10 s

Make every route declare what it produces, so a route only offers representations it opted into; give renderers explicit priorities instead of registration order; and set the unmatched-Accept policy per route class, not application-wide.

solid answer

~50 s

The failure mode in a shared application is that the renderer registry is **global mutable configuration** while routes are local: a registration anywhere re-decides selection everywhere that did not constrain itself. Three rules contain it. First, **routes declare their produced types** — opt-in beats inherit, and a route that declares nothing should be treated as a defect, enforced by a startup check or an architecture test. Second, **priorities are explicit numbers**, never source-file order, so adding a renderer cannot reorder incumbents. Third, **the unmatched-`Accept` policy is chosen per class of route**: machine-facing routes are usually better refusing with `406` than silently answering in a format the caller cannot read, while browser-facing routes often prefer serving the default. Introduce new representations additively and per route, and scope format overrides to where they are wanted rather than enabling them globally.

go deeper

for a junior

The idea to take away is that response-format configuration is shared across an application, so it needs owners and rules once more than one team deploys into it.

for a middle

Be able to explain why declaring produced types on a route is what stops a globally registered renderer from ever becoming a candidate for it.

for a senior

Show the rollout: low priority first, opt routes in group by group, keep the default move as a separate announced change, and pin representations in tests.

for a principal

Price the tradeoff rather than dismissing it. Mandatory declarations and explicit priorities cost boilerplate on every route and buy a registry where additions cannot change existing behaviour.

## The shape of the problem In a single-team service the renderer registry is invisible: one or two renderers, one format, nothing to govern. In a shared application it becomes the most quietly coupled piece of configuration in the system, for a structural reason: - the **registry is global** and additive — anyone, including a dependency, can add to it; - the **route is local** but frequently declares nothing, so it inherits the whole registry; - selection is **implicit at request time**, so a change is not visible in any diff of the affected route. Every governance decision below is an attempt to move that coupling from implicit to declared. ## Four decisions to make deliberately ### 1. Opt-in, not inherit Require each route to declare the media types it produces. The cost is boilerplate on every handler; the return is that the candidate set is bounded by something a reviewer of that route can see. A route with no declaration is not 'flexible' — it is a route whose behaviour is decided by the current contents of a global registry. Enforce it mechanically. A startup assertion or an architecture test that fails the build on an undeclared route is worth more than a convention in a style guide, because the failure this prevents appears months later in someone else's service. ### 2. Explicit priority, not registration order Where two renderers can serve the same request equally, something must break the tie. If that something is the order registration code happened to run, then the tie-break is a property of the dependency graph. Give each renderer a declared priority and treat a collision at the same priority as a configuration error to be surfaced at startup. ### 3. Strictness per class of route, not per application When no registered renderer can satisfy the request, the framework either refuses with `406 Not Acceptable` or answers with its default. A single global setting is the wrong granularity, because the two audiences want opposite things: | Route class | Preferred unmatched policy | Reason | |---|---|---| | Machine-facing, contract-bound | Refuse | A caller that asked for one representation and silently received another will misparse it, far from the cause | | Browser-facing or human-facing | Serve the default | Refusing a request you could have answered is a worse experience than a slightly unexpected representation | | Internal diagnostics and tooling | Serve the default | Availability matters more than exactness, and the audience can read anything | Make the choice a per-route-group setting with an explicit default, and write it down. The interview signal is recognising that this is a **policy** decision with an owner, not a framework detail. ### 4. Overrides are scoped, not ambient A format override in the URL is a debugging affordance that quietly becomes a public contract the moment a customer discovers it. Decide per route group whether overrides are enabled, prefer the query-parameter form over the path-suffix form, and keep the token table small — every token you accept is one you must keep honouring. ## Rolling out a new representation The governed version of 'we added a new format' is additive and reversible: 1. Register the renderer with a **low priority**, so it can never win a tie against an incumbent. 2. Opt routes in **one group at a time** by adding to their declared produced types. 3. Require callers to ask for it explicitly, so the change is invisible to everyone who does not. 4. Only after the new representation has real traffic, consider whether any route's default should move — and treat that as its own change, announced separately. A new representation introduced by changing the application default is the same change with the blast radius inverted. ## Guardrails worth building once - **A registry dump at startup**, logged: every renderer, its declared produced types, its priority. - **A conformance test per route group** pinning the representation for the header shapes real callers send, including the no-`Accept` case. - **A startup check** that every route declares produced types and that no two renderers collide at one priority. - **A review rule** that adding a globally registered renderer requires naming which route groups it is intended for. ## The tradeoff to state out loud All of this trades convenience for reviewability. A registry that just works with no declarations is genuinely faster to start with, and for one team it may never bite. The moment several teams deploy from one application, implicit selection means any of them can change the others' observable behaviour without touching their code — and that is the cost a principal is expected to price, not eliminate on principle.

  • Why set the unmatched-Accept policy per route group rather than once for the application?
    Because the audiences want opposite failures. A contract-bound caller that asked for one representation and silently received another will misparse it far from the cause, so refusing with `406` is cheaper. A human-facing route refusing a request it could have answered is a worse outcome than an unexpected but readable representation. One global setting is guaranteed wrong for one of the two.
  • How do you add a new representation without changing any existing caller's behaviour?
    Register its renderer at a priority that cannot win a tie, then opt routes in one group at a time by extending their declared produced types. Callers that do not ask for it keep ranking exactly as before. Moving any route's default to the new type is a separate, announced change — bundling the two is what turns an addition into an incident.
  • What single piece of instrumentation pays for itself fastest here?
    A startup dump of the registry: every renderer, its declared produced types, and its priority. It turns the one piece of global state nobody can see in a diff into something greppable in logs, and it makes a dependency that self-registers a renderer visible on the deploy that introduced it rather than during the incident it later causes.

saying these in an interview costs you the question

  • Registers every renderer globally and assumes routes are unaffected.
  • Relies on source-file registration order as the priority contract.
  • Applies one unmatched-Accept policy to browser and machine callers alike.
  • Introduces a new representation by moving the application-wide default.
  • Enables format overrides application-wide as a convenience.
  • Treats an undeclared route as flexible rather than as unconstrained.