How do you keep one internal failure contract when two surfaces of the same service must serialize errors in different wire formats?
answer
- format is a rendering decision
- one neutral value, several renderers
- handlers never learn the surface
- do not model on one standard's shape
- codes added, never re-pointed
basics
~20 sSplit the failure value from its rendering. One factory produces a format-agnostic failure value; a renderer chosen per surface serializes it into that surface's standard. Handlers raise failures and never learn which surface they are serving.
solid answer
~40 sTreat the wire format as a rendering decision, not a modelling one. The factory produces a failure value with your own vocabulary - a stable code, a message, the correlation id, declared structured detail - and knows nothing about field names on the wire. A renderer selected per surface, or by negotiated media type, maps that value onto the standard that surface publishes. Two consequences follow. The value model must not be shaped like one of the standards, or the second surface becomes a rewrite rather than a second renderer. And the code catalogue is what you version hardest: codes may be added, but an existing one must never be re-pointed, because both surfaces and every client have keyed on it. The cost is a doubled rendering test matrix.
go deeper
The idea to take away: the fields a client sees on the wire and the information the service collected about a failure are two different things, and one can change without the other.
Be able to describe the split - a factory produces a neutral failure value, a renderer serializes it for a surface - and why handlers must not know which format they are serving.
Show the migration mechanics: a second renderer selected by negotiated media type, measurement of which format each consumer receives, then a default flip and a retirement, rather than a coordinated cutover.
Own the ledger and the refusal. Price the doubled test matrix, forked documentation and split operational picture, protect the code catalogue as the stable asset, and be willing to say no to a second dialect that serves preference rather than obligation.
A service often has to speak more than one error dialect: an externally published surface that must follow a formal error standard, and an internal surface whose consumers use a leaner in-house shape. Or one standard is being migrated to another and both must be served during the transition. The question is how much of the service that difference is allowed to touch. ## Draw the seam at rendering, not at modelling There are three layers, and the discipline is to keep them apart: 1. **Failure** - what the code raises: a typed object naming the condition, carrying a stable code and any structured detail. 2. **Failure value** - what the factory produces: the neutral, format-agnostic record of code, message, correlation id and declared detail. This is your vocabulary, not anyone's standard. 3. **Rendering** - what the surface emits: field names, nesting, media type, and any members a published standard requires. Handlers only ever touch layer one. They do not learn which surface they are serving, because if they do, the knowledge of the wire format leaks back into hundreds of call sites and the second format stops being containable. | Layer | Changes when… | Should not change when… | |---|---|---| | Failure types | New domain conditions appear | The wire standard changes | | Failure value | The contract gains a concept (say, declared detail) | A surface renames a field | | Renderer | A surface adopts a different standard | New failure types are added | ## The modelling trap The common mistake is to build the failure value in the shape of whichever standard was adopted first - its field names, its nesting, its required members. It costs nothing on day one and is indistinguishable from the neutral model. Then the second surface arrives and every renderer is a translation between two foreign shapes, plus the value model quietly forces a concept the second standard does not have. Keep the value model in your own terms and both renderings are mappings out, which is the direction that stays cheap. ## Choosing the renderer Two selection mechanisms cover most cases, and they can coexist: - **By surface.** The route group, port or deployment the request arrived on determines the renderer, resolved once at the write boundary. Predictable and easy to reason about. - **By negotiation.** The renderer is chosen from what the client asked for, falling back to the surface's default when the request expresses no preference or an unsupported one. More flexible, and it makes a migration client-driven rather than a flag day. Either way the selection happens in one place. The moment renderer choice appears inside handler code, the seam has failed. ## The part to version hardest The field names are the least valuable thing in the contract and the codes are the most valuable. A client keying on a code has embedded a meaning in its own branching logic. So: - **Codes are added, never re-pointed.** Broadening or narrowing what an existing code means breaks consumers silently, because their branch still matches. - **Retiring a code is a deprecation with a window**, not a delete, even when the wire format around it changes. - **A new surface is not licence to renumber.** Both renderings should emit the same code for the same condition, or you have two catalogues and the failure taxonomy has forked. ## The costs a lead has to own Two renderings double a test matrix: every failure class now has to be asserted twice, and the end-to-end provocation suite runs per surface. Documentation forks. On-call engineers see two shapes in the same log pipeline unless the internal log record is normalised. None of that is fatal, but it is the honest ledger, and it is the argument for the decision an interviewer is really probing: **when to refuse the second format.** A request for a second dialect because one team prefers different field names is a request to double a test matrix for aesthetics. A published external standard the organisation is obliged to follow, or a migration with real consumers on both sides, is worth it - and both are bounded, with a defined end state. ## The judgment being assessed A strong answer lands three things: the seam is at rendering and handlers never learn the format; the neutral value model is deliberately not shaped like any one standard; and the code vocabulary is the stable asset, protected more carefully than the field names around it. A weaker answer treats the question as an implementation puzzle and reaches for a second factory - which is the same drift the shared factory existed to prevent, only now at the level of the whole contract.
- Why not just add a second failure-body factory for the second surface?Because that forks the contract at exactly the level the shared factory was built to protect. Two factories drift in what they include, how they treat unmapped failures and which codes they emit, and the second one usually loses a rule the first one learned the hard way. One value producer with two renderers keeps the policy single and makes only the serialization vary.
- How would you migrate all clients from one error wire format to another without a flag day?Add the new renderer alongside the old one and select by negotiated media type, defaulting to the old format. Clients opt in by asking for the new type, so migration is client-paced and reversible per client. Measure which format each consumer is actually receiving, chase the stragglers, then flip the default and finally retire the old renderer once the metric reaches zero.
- What is the strongest reason to refuse a request for a second error format?That it buys nothing a consumer genuinely needs. A second dialect permanently doubles the rendering test matrix, forks the documentation and puts two shapes into the same operational picture. A published standard the organisation must follow or a real migration justifies that; a team preferring different field names does not, and the answer is to point them at the existing renderer.
- How do you keep the two renderings from developing separate code catalogues?Make the catalogue an artefact of the value layer, above both renderers, and forbid a renderer from inventing or translating codes - it may only map the value's code into its own field. Test that both renderers emit the same code for the same provoked failure, so a fork shows up as a failing assertion rather than as a client bug months later.
saying these in an interview costs you the question
- Adds a second factory instead of a second renderer
- Models the neutral failure value on the first standard adopted
- Lets handler code learn which surface or format it is serving
- Re-points an existing error code when the wire format changes
- Lets each rendering grow its own code catalogue
- Accepts a second format without pricing the doubled test matrix