Spring Boot — Operations
Making a service observable, resilient and safe to run. These are cheap at the start and expensive to retrofit.
Observability
- Actuator with liveness and readiness probe groups. Readiness fails while dependencies are down, so traffic is not routed to a pod that cannot serve.
- Micrometer for metrics, OpenTelemetry for traces. Instrument before you need it —
../principles/measure-first.mdrequires a number, and you cannot get one after the incident. - Propagate a trace/correlation ID through every inbound and outbound call, and include it in every log line. An error you cannot correlate across services is an error you cannot diagnose.
- Structured JSON logs. Log the decision and its inputs, never secrets, tokens or personal data.
- Expose only
healthpublicly; secure every other Actuator endpoint —../principles/least-privilege.md.
Resilience
- Every outbound call gets a connect and a read timeout. Framework defaults are frequently infinite, and one hung dependency then exhausts the thread pool and takes the service down.
- Retry with exponential backoff and jitter, bounded attempts — and only for idempotent operations. A blind retry on a payment charges twice.
- Circuit-break calls to anything you do not control, with a defined fallback. Failing fast beats queueing work behind a dead dependency.
- Enable graceful shutdown with a drain period so in-flight requests finish during a deploy.
- Size the Hikari pool deliberately against the database's connection limit. The default is a guess, and pool exhaustion presents as unexplained latency.
Security
- OAuth2 resource server with JWT. Validate signature, issuer, audience and expiry — all four. A token that is merely well-formed is not a token that is valid.
- Authorise at the use-case boundary (
@PreAuthorizeor an explicit check), not only in the controller, and deny by default. - CORS as an explicit origin allowlist. Never
*with credentials. - Secrets from the environment, validated at startup. Never in
application.yml, never in version control, never in a log line.
Smells
No readiness probe, a RestTemplate with no timeout, a retry on a non-idempotent call, @PreAuthorize on nothing, cors().allowedOrigins("*") with credentials, metrics added during an incident, a trace ID that stops at the service boundary, secrets in application.yml.