37. Versioning, authz, validation, errors, logging and observability
Answer as submitted
“Yes — these six concerns are wired once in the composition root rather than recalled at each endpoint.”
Expanded answer
Yes, and the organising principle is that a cross-cutting concern applied per endpoint is a concern that will be forgotten at one endpoint. Versioning: the version lives in the URL because that is what a client pins and can see in its logs, the api-version header is accepted as a second route for internal callers behind a gateway, and a missing version is a 400 rather than a silent default — because AssumeDefaultVersionWhenUnspecified looks helpful and is exactly how a new client binds to v1 forever. Deprecation is announced with a Sunset header plus telemetry on who is still calling it, and the OpenAPI document is generated per version. Authentication and authorization: a JWT validated against the issuer's JWKS, cached and refreshed on a key-id change, with a named policy per endpoint and a fallback policy so that a new endpoint is protected by default — the failure mode of forgetting should be an endpoint nobody can call, not one everybody can. Identity comes from validated claims and never from the payload, and a resource belonging to another tenant answers 404 rather than 403 so existence is not disclosed. Validation is three layers with three jobs: the edge checks shape and format and returns every failure at once, the domain checks the business rules and can only be reached through its aggregate, and the database keeps the constraint that survives two concurrent requests. A rule duplicated across layers is a bug. Exception handling is one handler for the pipeline that maps known failures to the right status with a machine-readable problem type, and turns an unexpected one into a 500 carrying a trace id and nothing else — no stack trace, no SQL text, no internal path. Known failures are mapped rather than caught, and expected outcomes log at Information so a real incident is not buried in anticipatable noise. Logging and observability: structured logs inside a correlation scope opened at the first middleware and closed even on failure, the same id propagated through every call and message, OpenTelemetry traces that carry it, metrics that answer rate/errors/duration per endpoint with high-cardinality dimensions deliberately forbidden, personal data redacted at the sink rather than at each call site, and health checks that separate liveness from readiness — because a liveness probe that checks a dependency restarts healthy pods when the dependency is down, which turns a partial outage into a total one.
| Concern | Decision | Failure it prevents |
|---|---|---|
| Versioning | URL primary, header accepted, no silent default, Sunset on deprecation | a client silently bound to v1 forever, and a breaking change nobody was warned about |
| Authentication | JWT validated against the issuer JWKS, cached, refreshed on key rotation | a key rotation that takes the API down, or validation disabled for speed |
| Authorization | a named policy per endpoint plus a fallback policy, claims never the payload | an endpoint added in a hurry that anyone can call |
| Validation | edge for shape, domain for rules, database constraint as the last defence | rules duplicated in three places, drifting apart with each change |
| Exception handling | one handler, mapped failures, 500 with a trace id and no internals | stack traces and SQL text reaching a client, or a 500 nobody can trace |
| Logging and observability | structured logs in a correlation scope, traces, RED metrics, redaction at the sink | an incident investigated by guessing, and PII in a log index |
Live API cross-cutting workflow proof
The running component below changes its concern/result view through real event binding. Select a concern to see the decision, the reasoning behind it, and the evidence that it holds.
Choose a cross-cutting concern
Select a concern to inspect the decision, the reasoning and the evidence.
$ /api/v1/orders (primary) · api-version header (also supported) · no version → 400 · Sunset header on deprecated versions
The version lives in the URL because it is the version clients actually pin to, with the header accepted as a second route for internal callers. A request without a version is a 400 rather than a silent default, which is what stops a new client from accidentally binding to v1 forever. Deprecation is announced with a Sunset header plus telemetry on the callers still using it, and the OpenAPI document is generated per version.
versions served concurrently: 2 requests without a version: 400, never a silent default callers on the deprecated version: tracked, by client id deprecation announced with a Sunset header: yes
What wiring the concerns once buys
Source files
Select a file to inspect the proof with syntax highlighting and line numbers. The first file is the code running the component above; the others are the composition root that wires versioning, authz and validation, the single exception handler with its mapping table, and the observability configuration.
Acceptance criteria
- Addresses all six concerns the question lists, each with a decision behind it.
- Makes the safe behaviour the default, so an omission fails closed rather than open.
- Shows an interactive proof of choosing a concern and reading the evidence.
- Keeps internals out of error responses and keeps the diagnostics on the server, linked by a trace id.
- Configures logging and observability as reviewable code, with redaction and cardinality limits.