Skip to content

Clean Architecture

Clean Architecture organizes software around behavioral boundaries and dependency rules that protect core policies from external details.

This page shows how ForgingBlocks concepts can be projected onto a Clean Architecture arrangement.

Important

ForgingBlocks does not enforce Clean Architecture.
This page presents it as an interpretation, not a required structure.


Quick summary

Clean Architecture organizes software around behavioral boundaries and dependency rules that protect core policies from external details. This page shows how ForgingBlocks concepts can be projected onto this arrangement — not enforced.

Mapping:
- Inner layers — Domain (Entities, Value Objects) + Application (Use Cases, Handlers)
- Outer layers — Delivery mechanisms, technical details (Frameworks, Drivers, Interface Adapters)
- Dependencies always point inward

Fits when: long-term maintainability matters; strict policy/detail separation needed; multiple delivery mechanisms expected.
Consider alternatives when: simplicity > flexibility; strict rules add overhead; system is small/short-lived.


Conceptual mapping

  • The inner layers contain Domain and Application policies.
  • The outer layers contain delivery mechanisms and technical details.
  • Dependencies always point inward.

The diagram below shows the canonical Clean Architecture view from the literature, independent of ForgingBlocks.

---
title: Clean Architecture
---
graph TD
    Frameworks[Frameworks & Drivers<br/>DB, Web, External APIs] -->|implement| Adapters[Interface Adapters<br/>Controllers, Presenters, Gateways]
    Adapters -->|execute/handle| Application[Application Business Rules<br/>Use Cases, Handlers]
    Application -->|coordinate| Domain[Enterprise Business Rules<br/>Entities, Value Objects]

ForgingBlocks in practice

1. Dependency Inversion — Application defines the port, Infrastructure implements it

The application layer declares what it needs as an abstract OutboundPort.
The infrastructure layer provides the concrete implementation, keeping the
domain and application layers free from infrastructure details.

# === Application layer (port definition) ===
from abc import abstractmethod
from collections.abc import Sequence

from forging_blocks.application.ports.outbound import RepositoryPort


class OrderRepositoryPort(RepositoryPort["Order", str]):
    """Contract for persisting and retrieving orders."""

    @abstractmethod
    async def find_by_customer(self, customer_id: str) -> list["Order"]:
        """Retrieve all orders for a given customer."""
        ...


# === Infrastructure layer (adapter) ===
class InMemoryOrderRepository(OrderRepositoryPort):
    def __init__(self) -> None:
        self._store: dict[str, "Order"] = {}

    async def get_by_id(self, id: str) -> "Order | None":
        return self._store.get(id)

    async def list_all(self) -> Sequence["Order"]:
        return list(self._store.values())

    async def save(self, aggregate: "Order") -> None:
        self._store[str(aggregate.id)] = aggregate

    async def delete_by_id(self, id: str) -> None:
        self._store.pop(id, None)

    async def find_by_customer(self, customer_id: str) -> list["Order"]:
        return [o for o in self._store.values() if o.customer_id == customer_id]

2. UseCase depending on ports through constructor injection

The use case depends only on abstractions — never on concrete adapters.
Dependencies are injected at construction time.

# === Application layer (use case) ===
from dataclasses import dataclass

from forging_blocks.application.ports.inbound import ApplicationServicePort
from forging_blocks.application.ports.outbound import UnitOfWorkPort


@dataclass(frozen=True)
class PlaceOrderRequest:
    customer_id: str
    items: list[str]


@dataclass(frozen=True)
class PlaceOrderResponse:
    order_id: str


class PlaceOrderUseCase(ApplicationServicePort[PlaceOrderRequest, PlaceOrderResponse]):
    def __init__(
        self,
        order_repo: OrderRepositoryPort,
        uow: UnitOfWorkPort,
    ) -> None:
        self._order_repo = order_repo
        self._uow = uow

    async def execute(self, request: PlaceOrderRequest) -> PlaceOrderResponse:
        async with self._uow:
            order = Order.create(request.customer_id, request.items)
            await self._order_repo.save(order)
            return PlaceOrderResponse(order_id=str(order.id))

3. Domain AggregateRoot with business rules, independent of outer layers

The domain model contains pure business logic. It has no knowledge of
repositories, use cases, or infrastructure.

# === Domain layer ===
from uuid import UUID, uuid4

from forging_blocks.domain.aggregate_root import AggregateRoot
from forging_blocks.domain.messages.decorators import event_dataclass
from forging_blocks.domain.messages.event import Event


@event_dataclass
class OrderPlaced(Event[dict[str, object]]):
    order_id: str
    customer_id: str
    items: list[str]


class Order(AggregateRoot[UUID, dict[str, object]]):
    MAX_ITEMS = 20

    def __init__(self, order_id: UUID) -> None:
        super().__init__(order_id)
        self._customer_id: str = ""
        self._items: list[str] = []

    @property
    def customer_id(self) -> str:
        return self._customer_id

    @classmethod
    def create(cls, customer_id: str, items: list[str]) -> "Order":
        if len(items) > cls.MAX_ITEMS:
            raise ValueError(f"Order cannot exceed {cls.MAX_ITEMS} items")
        order = cls(uuid4())
        order.apply(OrderPlaced(
            order_id=str(order.id),
            customer_id=customer_id,
            items=items,
        ))
        return order

    def _handle(self, event: Event[dict[str, object]]) -> None:
        if isinstance(event, OrderPlaced):
            self._customer_id = event.customer_id
            self._items = event.items

When this style fits

  • Long-term maintainability is a priority.
  • Strict separation between policy and details is required.
  • Multiple delivery mechanisms are expected.

When to consider alternatives

  • Simplicity outweighs flexibility.
  • Strict dependency rules add unnecessary overhead.
  • The system is small or short-lived.