ADR-0013: Hexagonal Architecture as Standard for cornerstone-builder Archetypes
- Status: Accepted
- Date: 2026-03-20
- Deciders: DeAcero Agentic Team
- Supersedes (partially): ADR-0011 §Output structure
Context
The cornerstone-builder (ADR-0011) scaffolds new projects from verified feature files.
The initial mcp-only archetype produced a flat server.py containing domain logic,
I/O access, and MCP adapter code in a single file. This creates three concrete problems:
-
Unit testing is impossible without the MCP framework. To test whether
get_side_effectsclassifies a SP correctly, you have to stand up a FastMCP server and call it via a client. The domain logic (_is_external(), edge filtering) is entangled with the delivery mechanism. -
The delivery mechanism cannot be swapped. If the team decides to expose the same logic via a REST API for a different consumer, there is no path that does not involve rewriting the business logic.
-
Feature files cannot drive implementation cleanly. When
generate_stubs.pygenerates stubs from.featurefiles, it has no structural target: everything lands inserver.pymaking the file grow without bound and conflating layers.
The canonical solution for this class of problem is Ports and Adapters (hexagonal architecture, Alistair Cockburn, 2005): define the application core as pure business logic, surround it with abstract port interfaces, and plug concrete adapters to the outside.
Development protocol alignment
The team follows Features → ADRs → Tests → Code. Hexagonal architecture maps cleanly
onto this protocol:
- Features describe behavior of the domain service (not the delivery mechanism)
- ADRs document port/adapter boundaries as they are decided
- Tests exercise the domain in isolation (unit), adapters in isolation (integration),
and the full stack end-to-end (BDD/features)
- Code fills in stubs generated per layer
Decision
All archetypes produced by cornerstone-builder MUST follow the hexagonal directory layout:
src/<package>/
├── domain/
│ ├── __init__.py
│ └── service.py ← pure Python business logic; zero framework imports
├── ports/
│ ├── __init__.py
│ └── interfaces.py ← Protocol/ABC definitions for inbound and outbound ports
└── adapters/
├── inbound/
│ └── mcp.py ← FastMCP tools; thin wrappers — one line per tool body
└── outbound/
└── store.py ← concrete I/O; implements outbound port protocols
Layer invariants (enforced by tools/check_hexagonal_structure.py in CI)
| Layer | Rule |
|---|---|
domain/ |
MUST NOT import from adapters/, fastmcp, fastapi, sqlalchemy, os for I/O |
ports/ |
MUST NOT import from adapters/; only stdlib typing and abc |
adapters/inbound/ |
MUST import from ports/ or domain/; MUST NOT bypass domain by calling outbound/ directly |
adapters/outbound/ |
MUST implement a Protocol from ports/; MUST NOT contain business logic |
Test layer alignment
| Test layer | Target | What it imports |
|---|---|---|
tests/unit/ |
domain/service.py |
domain + a stub/mock for outbound port |
tests/integration/ |
adapters/inbound/ |
FastMCP test client; real or stub outbound adapter |
tests/features/ |
full stack via step defs | pytest-bdd; wires all layers together |
Archetype-specific adapter filenames
| Archetype group | Inbound adapter | Outbound adapter |
|---|---|---|
mcp-* |
adapters/inbound/mcp.py |
adapters/outbound/store.py |
*-api |
adapters/inbound/api.py |
adapters/outbound/db.py |
worker, mcp-worker |
adapters/inbound/worker.py |
adapters/outbound/queue.py |
scheduled-job |
adapters/inbound/scheduler.py |
adapters/outbound/db.py |
etl-pipeline |
adapters/inbound/runner.py |
adapters/outbound/source.py, adapters/outbound/sink.py |
library |
(none — no inbound adapter) | (none — no outbound adapter) |
cli-tool |
adapters/inbound/cli.py |
adapters/outbound/store.py |
Multi-stage Dockerfile standard
Every archetype includes a two-stage Dockerfile:
- builder stage: installs dev dependencies, runs ruff, mypy, pytest tests/unit/
- runtime stage: production dependencies only; CMD runs the inbound adapter
docker-compose.yml: always present
docker-compose.yml is generated for all archetypes. The app service is always defined.
Archetype-specific services (Redis for workers, frontend for UI) are appended by the hook.
Consequences
Positive
pytest tests/unit/ -vruns in milliseconds — no Docker, no MCP client, no DB- Delivery mechanism (MCP vs REST vs CLI) is swappable with zero domain changes
generate_stubs.pyhas structural targets: domain methods + port methods + adapter wrappers- CI can enforce layer boundaries statically via
check_hexagonal_structure.py - The BDD pipeline (
Features → ADRs → Tests → Code) maps 1:1 onto the four layers
Negative
- Minimum 7 files per archetype vs 1 (
server.py) — steeper first-project learning curve - Dependency injection is manual (constructor injection); no DI framework means boilerplate
in the composition root (
server.py/__main__.py) - Teams unfamiliar with hexagonal need onboarding before the pattern helps them
Mitigation
generate_stubs.pyhandles the boilerplate generation — teams rarely write layer stubs by handcornerstone-builderSKILL.md guides agents through the structure- The composition root (
server.py) is always < 20 lines by design
Dependencies
- ADR-0011 (cornerstone-builder) — this ADR refines the output structure defined there
tools/generate_stubs.py— enforces correct layer targeting (created per ADR-0011 §pending)tools/check_hexagonal_structure.py— CI enforcement (new tool, part of this ADR)