skip to content

How would you decide whether exception-to-status mapping lives in one central registry or in per-module handlers across a large service?

level: principalimportance: should knowfreq 43%

answer

  1. consistency versus autonomy
  2. separate vocabulary from membership
  3. small closed set of categories
  4. modules map onto categories, not statuses
  5. alert on fallback rate as drift signal

basics

~20 s

Centralise the vocabulary, distribute the membership. Define a small shared set of failure categories with one status each in a central registry, and let each module map its own failure types onto those categories rather than onto statuses directly.

solid answer

~50 s

The two pure positions both fail at scale. A single central registry listing every module's failure types becomes a file one team owns and every team edits, and it couples the transport layer to domain internals. Fully independent per-module handlers give the same class of failure different statuses depending on which module threw it, which callers experience as an inconsistent API. The design that holds is a **shared, small set of failure categories** — each with exactly one status and one body shape, registered centrally — plus **module-local mapping of that module's own types onto a category**. The central table then has a handful of entries that rarely change, and adding a domain failure never means editing shared code. Enforce it by making the categories the only thing the transport layer knows about, and by testing at the boundary that every module's failures resolve to a category rather than to the fallback.

go deeper

for a junior

Focus on the simpler half first: whatever the organisation chooses, your module's failures should map through a shared rule, not through statuses you pick per endpoint.

for a middle

Be able to argue both pure positions and name their failure modes — a bottleneck shared file on one side, the same concept answered with different statuses on the other.

for a senior

Propose the split between a small shared category set and module-local membership, and say how you would verify it: boundary tests per module and an alert on the unmapped fallback rate.

for a principal

Own it as governance. Decide who may extend the vocabulary, forbid broad registrations outside the shared registry, publish the categories as part of the API contract, and state the conditions under which the simpler central table wins instead.

## The two pulls **Centralise everything.** One registry, one file, every exception type in the service listed with its status. It gives perfect consistency and a single place to review the API's failure contract. It also becomes a bottleneck: every team edits it, it grows to hundreds of entries, and the transport layer ends up importing types from every corner of the domain — a dependency direction most architectures work hard to prevent elsewhere. **Distribute everything.** Each module registers handlers for its own failures. Teams move independently and nobody edits shared code. But nothing keeps two modules from mapping the same concept differently, so callers see one concept answered inconsistently, the same broad registration gets copy-pasted into five modules, and the service's failure contract exists only as the union of things nobody has read together. ## The arrangement that survives Split the decision into **vocabulary** and **membership**. - The **vocabulary** is a small closed set of failure categories — on the order of six to ten — each with exactly one status, one body shape and a written definition of when it applies. It lives centrally and changes rarely; adding a category is a deliberate review. - The **membership** is which of a module's failure types belongs to which category. That is domain knowledge, it changes constantly, and it belongs with the module. The transport layer then knows only the categories. A module contributes by making its failures carry or implement a category, or by registering a narrow local mapper that produces one. Two consequences follow: adding a failure type never touches shared code, and no module can invent a status. | Approach | Consistency | Team autonomy | Coupling | Where it breaks | |---|---|---|---|---| | One central table of every type | High | Low | Transport depends on all domains | Grows to a bottleneck file | | Fully independent per-module handlers | Low | High | Low | Same concept, different statuses | | Central categories, module membership | High | High | Modules depend on a tiny shared vocabulary | Category set must stay small | ## How to decide for a given service Weigh these, roughly in this order: 1. **How many teams touch the API surface?** One team can run a central table indefinitely; five cannot. 2. **Is the API one contract or several?** If different route groups deliberately expose different failure contracts, scoped registries per group are legitimate rather than a smell. 3. **How stable are the failure types?** Rapidly growing domains punish any design that requires a shared-file edit per new type. 4. **Who is accountable for the public contract?** Whoever must answer for "why does this return 409" should own the category set, and nothing else. 5. **What is your review capacity?** A distributed scheme only works if there is an automated check; without one it decays to independent per-module mapping within a year. ## Making the boundary real - **Keep the category set closed** and require a review to extend it — that review is the actual governance moment. - **Forbid registering on the broadest failure type outside the shared registry.** A module-scoped catch-all is how a module quietly acquires status authority. - **Test at the boundary, not the unit.** For each module, assert that its representative failures resolve to the intended category, and that nothing resolves to the generic fallback by accident. - **Alert on fallback rate.** A rising share of failures landing on the unmapped path means the membership is drifting behind the domain. - **Publish the categories as documentation**, because they are the failure half of the API contract that callers actually integrate against. ## The tradeoff to state out loud This design buys consistency and autonomy at the cost of **expressiveness**: a module that genuinely needs a status outside the vocabulary must either argue for a new category or accept the nearest one. That friction is the point — it is what stops the vocabulary from becoming the union of everyone's preferences — but it is a real cost, and in a service whose modules serve genuinely different audiences it may be the wrong trade. The honest principal answer names the conditions under which you would abandon it: few teams, one shared audience, and a stable domain make the plain central table simpler and better. What interviewers are listening for is whether you treat the mapping table as **API governance** rather than as configuration. The mechanics are easy; deciding who is allowed to say what a failure means to a caller is the part that has to be designed.

  • What signal tells you the arrangement is drifting?
    A climbing share of failures resolving to the generic fallback, new categories being proposed monthly, or the same concept appearing under two categories in different modules. Each means membership has fallen behind the domain or the vocabulary was cut too fine to be applied consistently.
  • When is a plain central table of every failure type the better choice?
    When one team owns the whole API surface, the domain is stable, and the failure list is small enough to read in one sitting. The bottleneck cost of a shared file is only real when many teams edit it, and the simpler design needs no governance to hold.
  • How do you stop a module from acquiring status authority informally?
    Forbid registrations on broad failure types outside the shared registry, and make the transport layer accept only categories. A module that can register a scoped catch-all has, in practice, the power to choose statuses for its routes regardless of what the central table says.
  • Should the category set be visible to API consumers?
    Yes. The categories are the failure half of the contract clients integrate against, so publishing them — with the status each maps to and when it applies — is what lets callers write retry and error-handling logic once instead of per endpoint.

A shipping company fixes a short list of service classes and their prices centrally; each depot decides which parcels belong in which class. Nobody invents a new price, and nobody in head office sorts parcels.

saying these in an interview costs you the question

  • Proposes one central file listing every failure type in every module
  • Lets each module choose statuses freely with no shared vocabulary
  • Treats the mapping table as configuration rather than API contract governance
  • Allows module-scoped catch-alls while claiming statuses are centrally controlled
  • Expands the category set on request until it mirrors the status code list