Skip to content

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:

  1. Unit testing is impossible without the MCP framework. To test whether get_side_effects classifies 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.

  2. 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.

  3. Feature files cannot drive implementation cleanly. When generate_stubs.py generates stubs from .feature files, it has no structural target: everything lands in server.py making 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/ -v runs in milliseconds — no Docker, no MCP client, no DB
  • Delivery mechanism (MCP vs REST vs CLI) is swappable with zero domain changes
  • generate_stubs.py has 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.py handles the boilerplate generation — teams rarely write layer stubs by hand
  • cornerstone-builder SKILL.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)