ADR-000: Hexagonal Architecture¶
Status: Accepted Date: 2026-01-10 Updated: 2026-10-01 Deciders: gepa-adk maintainers
Context¶
gepa-adk bridges two external libraries (GEPA's evolutionary optimization and Google ADK's agent execution framework). We need an architecture that:
- Keeps core evolution logic independent of external dependencies
- Makes external library integration clean and testable
- Allows swapping implementations without changing business logic
- Enables easy testing with mocks at boundaries
Decision¶
Adopt Hexagonal Architecture (Ports & Adapters) for gepa-adk with three distinct layers:
Layer Structure¶
gepa-adk/
├── domain/ # 🏢 CORE - Pure evolution logic
│ ├── models.py # EvolutionConfig, EvolutionResult, Candidate
│ ├── types.py # Type aliases, DTOs
│ └── exceptions.py # EvolutionError hierarchy
│
├── ports/ # 🔌 INTERFACES - Protocols
│ ├── adapter.py # AsyncGEPAAdapter protocol
│ ├── scorer.py # Scorer protocol
│ └── agent_provider.py # AgentProvider protocol (optional persistence)
│
├── adapters/ # 🔧 IMPLEMENTATIONS - ADK-specific
│ ├── evolution/adk_adapter.py # ADKAdapter implements AsyncGEPAAdapter
│ ├── scoring/critic_scorer.py # CriticScorer implements Scorer
│ └── workflow/workflow.py # Workflow utilities
│
├── engine/ # 🔄 ORCHESTRATION - Async engine
│ ├── async_engine.py # AsyncGEPAEngine
│ └── proposer.py # AsyncReflectiveMutationProposer
│
└── utils/ # 🛠️ UTILITIES
├── state_guard.py # State key preservation
├── events.py # ADK event parsing
└── schema_utils.py # Output schema helpers
Layer Rules¶
Layers are ordered adapters/ > engine/ > ports/ > utils/ > domain/. Each layer imports only layers below it.
| Layer | Can Import From | Cannot Import From |
|---|---|---|
adapters/ | ports/ (including the concrete defaults in ports/defaults.py), utils/, domain/, external libs (ADK, LiteLLM) | engine/ (receives the engine via injection) |
engine/ | ports/ (including ports/defaults.py), utils/, domain/, standard library, structlog | adapters/, google, litellm |
ports/ | utils/, domain/, standard library | adapters/, engine/, google, litellm |
utils/ | domain/, standard library, structlog, pydantic | adapters/, engine/, ports/, google, litellm |
domain/ | Standard library without IO modules, structlog | Every other layer, google, litellm, IO modules (os, io, logging, httpx, subprocess, socket, sqlite3, shutil, tempfile) |
ports/defaults.py holds the concrete default classes (such as the default selection strategies), so both engine/ and adapters/ reach them without a layer exception. Imports guarded by TYPE_CHECKING are exempt.
The [tool.importlinter] contracts in pyproject.toml enforce these rules through uv run lint-imports, in the pre-commit hook and the CI lint job. The separate boundaries shell script was retired on 2026-10-01 (#533).
Dependency Flow¶
┌─────────────────────────────────────────────────────────────┐
│ User Application │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ api.py (Public API) │
│ evolve(), evolve_sync() │
└─────────────────────────────────────────────────────────────┘
│
┌─────────┴─────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ engine/ │ │ adapters/ │
│ AsyncGEPAEngine │ │ ADKAdapter │
│ (depends on ports) │ │ CriticScorer │
└──────────────────────────┘ │ (implements ports) │
│ └──────────────────────────┘
│ │
└──────────────┬───────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ ports/ │
│ AsyncGEPAAdapter, Scorer protocols │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ domain/ │
│ EvolutionConfig, EvolutionResult, exceptions │
└─────────────────────────────────────────────────────────────┘
Consequences¶
Positive¶
- Testability: Core engine can be tested with mock adapters (no ADK required)
- Flexibility: Swap ADK adapter for alternative implementations
- Clear boundaries: External library changes don't leak into domain logic
- Maintainability: Each layer has single responsibility
- Dependency injection: Engine receives adapters, doesn't create them
Negative¶
- More files: More structure than a flat module
- Indirection: Must trace through layers to understand flow
- Protocol overhead: Defining interfaces adds initial development time
Neutral¶
- Learning curve: Team must understand port/adapter pattern
- Async everywhere: Async-first design permeates all layers
Alternatives Considered¶
1. Flat Module Structure¶
Rejected: Doesn't scale, mixes concerns, hard to test without external dependencies.
2. Service-Based Architecture¶
Rejected: Services tend to accumulate logic; hexagonal provides clearer boundaries.
3. Plugin Architecture¶
Rejected: Over-engineering for current scope; hexagonal is simpler.
References¶
- Hexagonal Architecture (Alistair Cockburn)
- Ports and Adapters Pattern
- ADR-002: Protocol for Interfaces (port definitions)
- ADR-005: Three-Layer Testing Strategy (testing each layer)
- ADR-006: External Library Integration (adapter implementations)
- ADR Index - All architectural decisions