Skip to content

Architecture

Components

graph LR
    A[pico-boot init] -->|entry point| B[pico_actuator]
    B --> C[ActuatorController]
    C -->|List injection| D[HealthIndicator components]
    C -->|List injection| E[InfoContributor components]
    C --> F[ActuatorSettings]
    C --> G[health.gather]

Four small modules, one responsibility each:

Module Responsibility
controller.py HTTP layer — routes, status codes. A pico-fastapi @controller.
health.py Pure aggregation logic. No FastAPI, no pico-ioc imports.
config.py ActuatorSettings + the two protocols.
exceptions.py PicoActuatorError.

Design decisions

Reuse pico-fastapi's @controller instead of an APIRouter. Route wiring, DI resolution and result normalization (the (body, status) tuple) are inherited for free, and the controller is a regular component — overridable in tests like any other.

Protocols + List[...] injection instead of a registry. HealthIndicator and InfoContributor mirror the FastApiConfigurer idiom: implement the protocol, mark the class @component, done. No decorators to import, no registration order to think about.

health.py stays pure. The aggregation logic imports nothing from the web stack, so it is unit-testable without booting an app — and reusable if a non-HTTP transport ever needs it.

Failure isolation over fail-fast. One broken indicator reports DOWN with its error; the endpoint itself never 500s. An actuator that dies with its dependencies is useless exactly when you need it.

Liveness is dependency-free by design. /health/live returns UP unconditionally: it proves the event loop answers, so orchestrators restart genuinely hung processes — not processes whose database blinked.