Knock rests on a simple principle: implement MicroProfile Health 4.0 with the JDK and Jakarta specs alone. No third-party library, no reflection-heavy machinery, no platform-thread pool. This page documents the implementation choices that uphold that discipline.

Module architecture

Module Responsibility

knock-mp-health-api

Vidocq fork of the MicroProfile Health API, repackaged with a module-info.java (see the Java Modules workaround below).

knock-api

Knock SPI: ProbeType, HealthCheckRegistry, Knock metadata. Re-exports the spec types.

knock-core

Standalone runtime: registry, aggregator, JSON-P serialiser, response builder, KnockHealthService facade. Pure Java SE.

knock-cdi-vauban

CDI-driven auto-registration of qualified checks on Vauban.

knock-jaxrs

Jakarta REST resource exposing /health* (deployed on Cassini).

knock-tck

Official MicroProfile Health 4.0 TCK runner — joins the reactor only under the tck Maven profile, has no module-info.java, and is never published to Maven Central.

Runtime flow

Diagram
  1. KnockAggregator.aggregate(HealthCheckRegistry, ProbeType) pulls the checks for the requested probe via HealthCheckRegistry.getChecks(ProbeType).

  2. Each HealthCheck.call() runs on its own virtual thread; the responses are reduced to a HealthSnapshot (spec §3.2 rules).

  3. KnockJsonSerializer.serialize(HealthSnapshot) renders the snapshot to a JSON String through Jakarta JSON-P; httpStatus(HealthSnapshot) maps UP/DOWN to 200/503.

  4. KnockHealthService.report(ProbeType) wraps both in a HealthReport(int httpStatus, String json) record.

Core classes

Class Role

KnockHealthCheckRegistry (internal)

HealthCheckRegistry implementation backed by a ConcurrentHashMap. No synchronized.

KnockAggregator (internal)

Applies the §3.2 aggregation rule and folds exceptions into DOWN checks named after the exception’s simple class name, with no data.

KnockJsonSerializer (internal)

Builds the response with Jakarta JSON-P (Json.createObjectBuilder()), no third-party JSON library.

KnockHealthCheckResponseBuilder (internal)

HealthCheckResponseBuilder implementation; types data entries (String, Long, Boolean, Integer, else String.valueOf).

KnockHealthCheckResponseProvider (internal)

HealthCheckResponseProvider SPI implementation registered via provides/META-INF/services.

HealthSnapshot (internal record)

Immutable aggregation result: probe type, overall status, checks list.

HealthCheckRegistries (runtime)

Exported factory — newRegistry() is the only public way to obtain a HealthCheckRegistry instance.

KnockHealthService (runtime)

Public facade tying registry + aggregator + serialiser together.

HealthReport (runtime record)

HTTP response wrapper: status code + serialised JSON.

CDI discovery

knock-cdi-vauban ships two internal pieces:

  • HealthCheckRegistrar — an @ApplicationScoped bean whose observer method onApplicationStart(@Observes @Initialized(ApplicationScoped.class) Object) registers each qualified HealthCheck into the registry by probe type, under the check’s class name (spec §4.1). It injects Instance<HealthCheck> qualified with @Liveness, @Readiness and @Startup; a HealthCheck bean with none of those qualifiers is never selected and is silently ignored (spec §4.2).

  • KnockCdiHealthCheckRegistry — the @ApplicationScoped registry bean, injectable as HealthCheckRegistry, delegating to an instance obtained from HealthCheckRegistries.newRegistry().

There is no BuildCompatibleExtension: both beans are instantiated, field-injected and notified through the _VaubanComponents provider generated at compile time by the Vauban annotation processor and declared as provides io.vidocq.vauban.api.VaubanComponentProvider in module-info.java — no runtime classpath scanning, no setAccessible(true), no opens to the container.

JAX-RS resource

KnockHealthResource is annotated @Path("/health") and exposes the four endpoints. Each method maps its path to a ProbeType, calls KnockHealthService.report(…​) on the injected HealthCheckRegistry, and returns a Response with the report’s HTTP status. The resource uses only the standard jakarta.ws.rs and CDI APIs — never a Cassini-internal package — and knock-jaxrs declares its own generated _VaubanComponents provider to wire the resource bean.

Threading model

All check execution goes through Executors.newVirtualThreadPerTaskExecutor() (JEP 444). Consequences:

  • a slow check never blocks the rest of a probe group;

  • no platform-thread pool, no bounded queue, no ThreadLocal;

  • shared state (the registry) uses ConcurrentHashMap exclusively.

Java Modules workaround (ADR-001)

The upstream MicroProfile Health API jar ships without a module-info.class, which breaks strict Java Modules and jlink. Knock forks it as knock-mp-health-api and rebuilds it with a proper module descriptor. To keep the descriptor from interfering with annotation processing, the module-info.java lives in src/main/module-info/ and is compiled in a dedicated phase at prepare-package. The SPI is registered twice — through Java Modules provides and META-INF/services — so the implementation resolves on both the module path and the classpath. Full rationale: docs/adr/ADR-001-java-modules-workaround-microprofile-health.md.

AOT compatibility

No dynamic proxy generation, no setAccessible(true), no hot reflection in the request path. Knock is native-image friendly and works inside a minimal jlink image.

Sources