Files
clean-architecture-backend-…/docs/adr/ADR-WS-001-platform-as-packages-and-its-boundaries.md

3.1 KiB

ADR-WS-001: The WebSocket platform ships as packages in one leaf, with machine-checked boundaries

  • Status: accepted
  • Date: 2026-08-25
  • Scope: :adapter:inbound:websocket

Context

The realtime connection platform design models itself as eighteen Gradle modules under modules/websocket, each with a declared purity grade and a declared set of allowed dependencies. This repository's src/config/architecture/modules.json is a fail-closed registry that owns the leaf list; adding eighteen leaves is a registry change of a size that needs its own decision, and HARD-STOP #5 forbids doing it implicitly.

Three earlier platforms in this repository — JPA, GraphQL, and the HTTP platform — met the same situation and resolved it the same way.

Decision

The eighteen design modules ship as packages inside the single registered leaf. WebSocketStableModule declares each one's package, purity grade and exact allowed edges, and WebSocketModuleBoundaryTest scans the production tree and fails when the declaration and the tree disagree in either direction.

Three deviations from the design's module map were forced by the check and are recorded in docs/websocket/repository-adaptation.md: WebSocketSubprotocolName moved to core and the codec moved to its own FRAMEWORK_BOUND module, both to avoid cycles the design's placement created here; and the budget -> core edge was inverted because budget imports nothing from core.

Consequences

The boundary is enforced, not documented. Six violations were caught during implementation that a document would not have: two would-be cycles, a duplicate module declaration where two ids claimed one package, and three undeclared edges. The duplicate is the instructive one — with two ids on one package, ownership depends on iteration order and one module's rules silently apply to nothing. A guard against it is now part of the boundary test.

The detector had a hole. Its framework-import list named com.fasterxml (Jackson 2) and not tools.jackson (Jackson 3), which is what Spring 7 actually uses — so a CORE module could have imported a mapper unnoticed. Fixed here and in the HTTP platform, which shared the list.

Promotion stays cheap. Each enum constant is already shaped like a leaf specification, so splitting one out later is a registry edit rather than an archaeology exercise.

The design's own rules were kept where they cost something. core names no framework, so the same decisions serve both runtimes and are testable without a server; no Java class name reaches the wire; the payload is an encoded string rather than a map; and handlers are given no way to write, which is what makes ordering and backpressure guarantees rather than conventions.

Alternatives considered

Register eighteen leaves. Faithful to the design and a large change to a fail-closed registry for a platform that ships as one artifact either way. Rejected as disproportionate; the boundary test provides the property the modules were for.

Ship the modules as packages with no enforcement. Cheapest, and it makes the boundary a claim. The six violations found during implementation are the argument against it.