hexagonal-architecture
Hexagonal architecture (ports/adapters) with DDD principles. Use when implementing features, refactoring code, organizing code structure, determining where code should be placed, creating adapters, services, ports, or when questions arise about architectural layers, dependency direction, adapter pat
Install
mkdir -p .claude/skills/hexagonal-architecture-michalwelna0 && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/15204" && unzip -o skill.zip -d .claude/skills/hexagonal-architecture-michalwelna0 && rm skill.zipInstalls to .claude/skills/hexagonal-architecture-michalwelna0
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Hexagonal architecture (ports/adapters) with DDD principles. Use when implementing features, refactoring code, organizing code structure, determining where code should be placed, creating adapters, services, ports, or when questions arise about architectural layers, dependency direction, adapter patterns, or service patterns.About this skill
Hexagonal Architecture + DDD
When to Use
- You are adding or refactoring features and need to place code in the correct layer (domain/service/port/adapter/controller).
- You are defining a new port or adapter and want to ensure dependency direction is correct.
- You are creating or modifying adapters, services, or controllers and need layer-specific conventions.
- You are reviewing imports/structure to enforce architectural boundaries.
Quick Reference
| Topic | File | When to Use |
|---|---|---|
| Project structure | project-structure.md | Canonical directory tree, code location mapping, layer responsibilities, naming conventions |
Controllers (Thin Layer)
Controllers are the entry points that connect external inputs to the application. Examples: a FastAPI app, a Click CLI, a Chainlit chat app. Keep them thin:
- Parse/validate inputs
- Call
sharedservices - Map errors to appropriate responses (e.g., HTTP status codes, CLI exit codes, UI messages)
- Serialize outputs via
model_dump(mode="json")or equivalent - No business rules in controllers
For FastAPI-specific conventions, see the fastapi skill.
For Click-specific conventions, see the cli-conventions skill.
Domain Models
- Put business rules/invariants in domain models (
shared/src/shared/domain/**) - Raise
ValueErrorwith clear messages for domain violations - Pydantic v2 is the sole allowed third-party dependency in domain models
For DDD building blocks (entities, value objects, aggregates), file organization, and the persistence contract, see the domain-models skill.
Services (Orchestration)
Services are use-case orchestrators in shared/src/shared/services/**.
For system health checks (technical, non-business concerns):
- Put domain models in
shared/src/shared/domain/platform_ops/health/ - Services should aggregate adapter results into a generic
SystemHealth(no per-adapter probe logic in services)
Inputs
- Fixed internal knobs (model IDs, temperatures, limits) live in a nested:
@dataclass(frozen=True, slots=True) class Hyperparams: ...- with
_HP: Final[Hyperparams] = Hyperparams()
- Controller-controlled parameters should be Pydantic v2 models named
*Params(typically inshared/src/shared/domain/**) and passed asparams=... - Direct inputs (what to act on) are primitive args alongside
params(e.g.,source_url: str,article_id: UUID)
Outputs
- Return domain models (Pydantic); let controllers serialize via
model_dump(mode="json") - If a workflow needs a composite result, prefer a dedicated domain response model over a raw
dict
Validation
- Validate service-level inputs early (bounds, URL format) using shared validators (e.g.,
validate_url) - Domain invariants live in domain models and raise
ValueError - Do not duplicate the same validation across service + domain layers
Resource Handling
- If a service receives closeable dependencies, the wiring layer (controller-side DI / setup) must own lifecycle and close them in
finally
Ports (Interfaces)
- Define boundaries as ports (
shared/src/shared/ports/**) usingtyping.Protocol - Minimal method surface; no side effects; no external library imports; return domain types
- Ports MUST be technology-agnostic: folder names describe business capabilities (e.g.,
ports/database/,ports/llm/), NOT technologies - Repository naming conventions:
- Single item:
get_{model}(...) -> Model | None(missing →None) - Lists:
list_{model}s(..., limit: int | None = None, offset: int = 0) -> list[Model](missing →[]) - Prefer one
list_*with multiple optional filters over specialized*_by_*methods
- Single item:
Adapters (Implementations)
Adapters live in shared/src/shared/adapters/** and are technology-specific (e.g., adapters/database/postgres/, adapters/llm/).
For adapters integrating external components (DB/LLM/HTTP/etc.), expose:
async def health_check(self) -> ComponentHealth- Use generic health VO models (
ComponentHealth,SystemHealth) and put component diagnostics indetails(e.g.db-version,base-url)
Validation
- Validate adapter inputs early and fail fast with
ValueError(non-empty strings, bounds, required IDs) - Avoid magic numbers: define timeouts/limits/constants at module top
Error Handling
- Wrap integration failures as
RuntimeErrorwith actionable messages - Keep original cause via
raise ... from e - Log failures at
errorlevel (no secrets in logs or exceptions)
Resource Cleanup
- If an adapter owns network/IO resources, implement
close()(async if needed) close()should be best-effort: log and continue on cleanup errors- The wiring layer (controller-side DI / setup) must always close adapters in
finally
Database (Repository) Adapters
Repository adapters follow the thin wrapper pattern:
- Delegate CRUD operations to generic helpers in
{db_engine}_aggregate_crud.py - Define
TABLE_NAMEandSTORED_MODEL_NAMEconstants at module top - Validate
dbconnection in__init__ - Wrap
asyncpg.PostgresErrorasRuntimeError(done in CRUD helpers, not repeated in adapters) - Return validated domain models (deserialized via
AggregateRoot.from_payload())
Dependency Direction
shared/src/shared/domain/**→ no dependencies on project code (standard library + Pydantic only)shared/src/shared/services/**andshared/src/shared/ports/**→ may depend only onshared/src/shared/domain/**shared/src/shared/adapters/**→ may depend onshared/src/shared/domain/**,shared/src/shared/services/**, andshared/src/shared/ports/**- Controller directories (e.g.,
api/,app/,cli/) → may depend onshared/(domain, services, ports, adapters), butshared/must not depend on controllers shared/src/shared/adapters/database/{db-engine}/migrations/**andshared/src/shared/config/**→ may depend on shared code but must not be imported from domain, services, or ports
Change Workflow Checklists
Adding New External Capability
- Add a new Port in
shared/src/shared/ports/ - Implement an Adapter in
shared/src/shared/adapters/.../ - Update/create a Service method in
shared/src/shared/services/to orchestrate via ports - Wire the adapter in the controller layer as needed (do not wire inside
shared)
Adding Database Read Model / Cross-Aggregate Lookup
- Add a QueryPort in
shared/src/shared/ports/database/queries/ - Implement the adapter in
shared/src/shared/adapters/database/.../queries/ - Use services to orchestrate; do not expose adapters directly to controllers
Adding a New Aggregate Repository
- Create a migration
- Add a repository port in
shared/src/shared/ports/database/repositories/ - Implement a repository adapter — thin wrapper delegating to
{db_engine}_aggregate_crudhelpers - Wire the adapter in the controller layer
Adding New Domain Rule
- Enforce in the domain model (entity/value object) with clear
ValueErrormessages - Add unit tests in
shared/tests/for both valid and invalid cases
Changing Error Message or Validation
- Update/extend tests to match the new behavior and message (tests commonly use
match=...)
Cross-References
- Project structure: See project-structure.md for canonical paths, directory tree, and layer responsibilities
- Domain models: See
domain-modelsskill for DDD building blocks - FastAPI: See
fastapiskill for API-specific conventions - CLI: See
cli-conventionsskill for Click-specific conventions - Python patterns: See
pythonskill for type hints and error handling