ADR 0003: Error Contract Governance
Ratification
Adopted before ADR 0018. There was no separate ratification process. Git history for this file on main is the record.
- Discussion Issue: not recorded (before ADR 0018)
- Merge PR: see git history for this file
- Accepted: as merged to
main
Context
Why this matters: Clients cannot rely on free-text messages; they change with wording and locale. Stable codes and keys let apps and partners branch on fixed values, log clearly, and show their own user-facing text. One-off errors also make incidents harder to fix: “something failed” in logs is not enough to act on.
As we add endpoints, we need one shared list of transport errors (auth, rate limits, idempotency) and domain-specific tables per entity, next to OpenAPI and tests.
Decision
We use one versioned error contract in code and OpenAPI:
- Error payloads are part of public API contract.
- Use stable numeric
codeand symbolickey. code+keyare immutable once published.- Contract evolves additively; existing semantics cannot be silently changed.
- Validation and business errors follow explicit, documented schemas.
- Error catalog is split into:
- COMMON pool for service-wide reusable errors (security/rate-limit/idempotency/core transport).
- Entity matrices for domain-specific errors (for example
USER_*).
Contract model
- Business error shape:
{"code":"...","key":"...","message":"...","source":"business"}
- Validation error shape:
{"error_type":"validation_error","endpoint":"...","errors":[...]}- each
errors[]item:code,key,message,field,source,details
Error catalog structure
Common reusable pool (service-wide)
Use COMMON_* for errors that should be reused consistently across all endpoints.
- Examples: auth required, rate-limit exceeded, body too large, idempotency key conflicts.
- Implementation: reusable OpenAPI response blocks in
app/openapi/responses.py. - Rule: do not duplicate these responses inline in routers unless behavior differs.
Entity matrices (domain-specific)
Use entity-prefixed codes for business/validation rules specific to one domain.
- Example:
USER_001...USER_013validation matrix andUSER_101,USER_404. - Rule: entity code meaning is immutable once released.
- Rule: each entity matrix must be reflected in OpenAPI examples and tests.
Implementation in this repository
- Shared OpenAPI response blocks:
app/openapi/responses.py
- Validation mapping source:
app/validation/user.py
- Error response schemas:
app/schemas/errors.py
- OpenAPI examples:
app/openapi/examples/errors.py
- Endpoint declarations:
- router
responses={...}inapp/api/v1/*.py
- router
Consequences
Positive
- Predictable client integration behavior.
- Better incident diagnostics and observability.
- Easier compatibility management across API versions.
Trade-offs
- Requires governance discipline and documentation updates.
- Expanding error catalog requires ongoing curation.
Operating rules
- Add new codes; do not repurpose old ones.
- Keep fallback validation codes for unmapped errors.
- Prefer shared
COMMONresponse blocks for cross-cutting errors. - Entity routers must keep their own matrix for domain-only errors.
- Update OpenAPI examples and docs in the same change.
- Ensure
make docs-fixandmake release-checkpass before release.
Page history
| Date | Change | Author |
|---|---|---|
| Added Page history section (repository baseline). | Ivan Boyarkin |