ADR-0019 — Introduce interfaces/ and bootstrap/ Layers¶
Status: Accepted
Amends: ADR-0018 “Adopt Hexagonal (Ports & Adapters) Package Structure”
Context¶
ADR-0018 established the hexagonal package layout and high-level import boundaries. Since then, we’ve tightened a key rule—adapters must not depend on the service layer or entrypoints—and we want a centralized place to assemble the application (a composition root). Two gaps emerged:
- A neutral location for application-level interfaces/DTOs that both the service layer and adapters can depend on without violating the adapter rule or polluting the domain.
- A bootstrap area to wire implementations, manage lifecycle/configuration, and optionally expose small facades to entrypoints without leaking adapter details.
Decision¶
Add two packages and clarify boundaries:
interfaces/
Neutral package for application-level interfaces (protocols/abstractions) and simple data carriers (DTOs) used across layers (e.g., database status provider, filestore maintenance, clocks/ID generation).
Why: lets adapters and the service layer meet at a shared contract without forcing adapters to import the service layer or pushing operational concepts into the domain. interfaces/ remains independent (no imports from calista.*).
bootstrap/
Composition root responsible for wiring concrete adapters to handlers, managing lifecycle/configuration, and composing shared services (e.g., message bus, unit of work). It may expose small facades for entrypoints as needed.
Why: keeps entrypoints adapter-agnostic and centralizes assembly concerns that don’t belong in the service layer.
Architecture & Dependency Rules¶
Layer order (outer → inner): entrypoints → bootstrap → adapters → service_layer → interfaces → domain
Constraints (clarified):
entrypoints/depends onbootstrap/only.bootstrap/may depend on adapters, service_layer, interfaces, and domain.adapters/may depend oninterfaces/anddomain/; not on service_layer or entrypoints.service_layer/may depend oninterfaces/anddomain/; not on adapters or entrypoints.interfaces/is independent (no imports fromcalista.*).domain/remains innermost.
(These constraints are enforced via Import Linter contracts in pyproject.toml.)
Consequences¶
- Pros: Clear seams; strict adapter rule upheld; entrypoints remain adapter-agnostic via
bootstrap/; easier testing/substitution; composition concerns (e.g., message bus, UoW) have a home. - Cons: Slightly more structure (one small extra package); import rules must remain green in CI.
Alternatives Considered¶
- Let adapters import
service_layer.ports. Simpler, but violates the strict adapter rule and couples adapters to application internals. - Put application interfaces in the domain. Blurs ubiquitous language and pollutes the domain with operational concerns.
- Skip
bootstrap/and wire in entrypoints. Leaks assembly details into UIs and weakens layering guarantees.
Relationship to ADR-0018¶
This ADR amends ADR-0018 by introducing interfaces/ and bootstrap/ and by tightening import rules. ADR-0018 remains valid; this ADR clarifies where app-level contracts live and where composition happens (including common shared services like the message bus and unit of work).