Hexagonal Architecture¶
Hexagonal Architecture, also known as Ports and Adapters, emphasizes separation between core behavior and external systems.
This page shows how ForgingBlocks concepts can be projected onto a hexagonal arrangement.
Important
ForgingBlocks does not enforce Hexagonal Architecture.
This page presents it as an interpretation of responsibilities defined in the Reference section.
Quick summary¶
Hexagonal Architecture (Ports and Adapters) emphasizes separation between core behavior and external systems. This page shows how ForgingBlocks concepts can be projected onto this arrangement — not enforced.
Mapping:
- Core — Domain (business rules) + Application (Use Cases, Handlers)
- Inbound Ports — Define how behavior is triggered (ApplicationServicePort, MessageHandlerPort)
- Outbound Ports — Define required external capabilities (RepositoryPort, MessageBusPort, UnitOfWorkPort)
- Adapters — Implement ports (Infrastructure: SQL repos, message brokers, HTTP clients)
- Dependencies point toward the core
Fits when: external systems change frequently; testing without infrastructure matters; inbound/outbound isolation needed.
Conceptual mapping¶
- The core contains Domain and Application logic.
- Inbound ports define how behavior is triggered.
- Outbound ports define required external capabilities.
- Adapters implement those ports.
- Dependencies point toward the core.
The diagram below shows a canonical hexagonal view from the literature, independent of ForgingBlocks.
---
title: Hexagonal Architecture
---
graph LR
InboundAdapters[Inbound Adapters<br/>HTTP, CLI, Events] -->|execute/handle| ApplicationCore[Application Core<br/>Use Cases & Handlers]
ApplicationCore -->|dispatch/persist/notify| OutboundAdapters[Outbound Adapters<br/>Repositories, Message Bus]
ForgingBlocks in practice¶
1. Inbound Port — defining how the core is triggered¶
An inbound port defines a contract for driving the application. Adapters
(HTTP, CLI, tests) call this contract without the core knowing about them.
from abc import abstractmethod
from dataclasses import dataclass
from forging_blocks.application.ports.inbound import ApplicationServicePort
@dataclass(frozen=True)
class RegisterCustomerRequest:
name: str
email: str
@dataclass(frozen=True)
class RegisterCustomerResponse:
customer_id: str
class RegisterCustomerUseCase(
ApplicationServicePort[RegisterCustomerRequest, RegisterCustomerResponse],
):
"""Inbound port — any adapter can trigger registration through this contract."""
@abstractmethod
async def execute(
self, request: RegisterCustomerRequest,
) -> RegisterCustomerResponse:
...
2. Outbound Port — defining what the core needs¶
An outbound port declares a capability the core requires, without specifying
how it is fulfilled. The core depends on this abstraction, never on the
concrete implementation.
from abc import abstractmethod
from uuid import UUID
from forging_blocks.application.ports.outbound import RepositoryPort
class CustomerRepositoryPort(RepositoryPort["Customer", UUID]):
"""Outbound port — the core needs customer persistence, full stop."""
@abstractmethod
async def find_by_email(self, email: str) -> "Customer | None":
"""Retrieve a customer by email address."""
...
3. Infrastructure adapter implementing the OutboundPort¶
The adapter fulfills the outbound port contract with a concrete technology.
Swap adapters without touching the core.
class InMemoryCustomerRepository(CustomerRepositoryPort):
def __init__(self) -> None:
self._store: dict[UUID, "Customer"] = {}
async def get_by_id(self, id: UUID) -> "Customer | None":
return self._store.get(id)
async def list_all(self) -> Sequence["Customer"]:
return list(self._store.values())
async def save(self, aggregate: "Customer") -> None:
self._store[aggregate.id] = aggregate # type: ignore[assignment]
async def delete_by_id(self, id: UUID) -> None:
self._store.pop(id, None)
async def find_by_email(self, email: str) -> "Customer | None":
for customer in self._store.values():
if customer.email == email:
return customer
return None
When this style fits¶
- External systems change frequently.
- Testing without infrastructure is important.
- Inbound and outbound interactions must be isolated.