agentskills.codes
HE

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

Installs 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.
327 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)

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

TopicFileWhen to Use
Project structureproject-structure.mdCanonical 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 shared services
  • 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 ValueError with 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 in shared/src/shared/domain/**) and passed as params=...
  • 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/**) using typing.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

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 in details (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 RuntimeError with actionable messages
  • Keep original cause via raise ... from e
  • Log failures at error level (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_NAME and STORED_MODEL_NAME constants at module top
  • Validate db connection in __init__
  • Wrap asyncpg.PostgresError as RuntimeError (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/** and shared/src/shared/ports/** → may depend only on shared/src/shared/domain/**
  • shared/src/shared/adapters/** → may depend on shared/src/shared/domain/**, shared/src/shared/services/**, and shared/src/shared/ports/**
  • Controller directories (e.g., api/, app/, cli/) → may depend on shared/ (domain, services, ports, adapters), but shared/ must not depend on controllers
  • shared/src/shared/adapters/database/{db-engine}/migrations/** and shared/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

  1. Add a new Port in shared/src/shared/ports/
  2. Implement an Adapter in shared/src/shared/adapters/.../
  3. Update/create a Service method in shared/src/shared/services/ to orchestrate via ports
  4. Wire the adapter in the controller layer as needed (do not wire inside shared)

Adding Database Read Model / Cross-Aggregate Lookup

  1. Add a QueryPort in shared/src/shared/ports/database/queries/
  2. Implement the adapter in shared/src/shared/adapters/database/.../queries/
  3. Use services to orchestrate; do not expose adapters directly to controllers

Adding a New Aggregate Repository

  1. Create a migration
  2. Add a repository port in shared/src/shared/ports/database/repositories/
  3. Implement a repository adapter — thin wrapper delegating to {db_engine}_aggregate_crud helpers
  4. Wire the adapter in the controller layer

Adding New Domain Rule

  1. Enforce in the domain model (entity/value object) with clear ValueError messages
  2. Add unit tests in shared/tests/ for both valid and invalid cases

Changing Error Message or Validation

  1. 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-models skill for DDD building blocks
  • FastAPI: See fastapi skill for API-specific conventions
  • CLI: See cli-conventions skill for Click-specific conventions
  • Python patterns: See python skill for type hints and error handling

Search skills

Search the agent skills registry