ADR-0006: One problem-document convention, with snake_case codes
- Status: Accepted
- Date: 2026-10-05 (originally decided 2026-10-03 and accepted 2026-10-04, re-recorded here on the second lift)
- Deciders: Process maintainer
Context
Section titled “Context”Every service returned RFC 9457 problem documents, and every spec agreed on the shape: a
URN type, application/problem+json, a field member on validation. Nothing said how a
code is spelled, so each implementation chose. When it was audited, one service served about
thirty kebab-case codes with one snake_case exception taken literally from its spec, another
served snake_case, a third served no codes at all and returned 422 for validation, and a
fourth had built nothing yet. One service spelled the same idea two ways, snake_case in an
event reason and kebab-case in a problem code.
Every code any admitted spec named was snake_case, and so was every event reason enum.
And nothing consumed a code yet: no client matched on type. A rename cost nothing that day
and would break a client later.
Decision
Section titled “Decision”- The problem document is a service convention,
spec/service-conventions.md§10: the members;typeisurn:<org>:<service>:error:<code>, derived in one place;about:blankonly for a problem with no code;titleper type;detailper occurrence and never reflecting input;fieldnamed as on the wire. - Codes are snake_case,
^[a-z][a-z0-9]*(_[a-z0-9]+)*$, the same rule as every other machine-matched value on the wire. - A short shared vocabulary for what every service meets at its transport layer:
validation_failed400,unauthorized401,forbidden403,not_found404,method_not_allowed405,rate_limited429,internal500,not_ready503. A service uses these and mints no synonym. - Every 400 carries
validation_failed, covering a request that cannot be read and a member that breaks its constraint.fieldnames the member whenever one is at fault. - Codes are contract. Adding one is additive. Renaming or removing one that a released contract served is breaking.
Alternatives considered
Section titled “Alternatives considered”- kebab-case. The strongest case against snake_case: RFC 9457’s own examples spell type
URIs in kebab-case, and event
typenames are kebab-case too. Rejected because a problem code is not a type name. It is a value a client switches on, the same kind of thing as an eventreason, and those were already snake_case. The RFC examples are examples, not a rule, and the eventtypeis a separate, dotted, versioned namespace a code never shares. - Each service chooses. Rejected: that was the finding. A client composing two services
meets
not_foundandnot-foundfor one idea. - Two validation codes (
invalid_requestfor unreadable,validation_failedfor a broken constraint). Costs a distinction no client was known to act on. - No shared validation code, a code per rule. Most specific, least uniform, and the only option under which two services can still spell validation differently.
Consequences
Section titled “Consequences”- One spelling across a system, written where every service vendors it, before the first client exists. A service’s code vocabulary becomes checkable by its own tests.
- A service that predates the convention renames its codes, and because the codes are its documented match key, that is a contract-version change for it.
- Deferred: a text gate over the API description that every
urn:<org>:<service>:error:literal names its own service and matches the pattern, seeded from the scaffold’s common tree with failing fixtures. Trigger: the first client that matches ontype, or the next service seeded, whichever comes first.