Clean Architecture per Microservizi FastAPI
Introduzioneβ
Questa guida documenta l'architettura Clean/Hexagonal adottata in tutti i microservizi FastAPI del progetto VISLA. L'obiettivo Γ¨ garantire:
- Separazione delle responsabilitΓ - Ogni layer ha un compito specifico
- TestabilitΓ - Business logic isolata e facilmente mockabile
- ManutenibilitΓ - Cambiamenti in un layer non impattano gli altri
- Indipendenza dal framework - Il domain non conosce FastAPI, SQLAlchemy, ecc.
Struttura Ideale di un Microservizioβ
service-name/
βββ app.py # Entry point FastAPI
βββ config.py # β οΈ LEGACY - spostare in infrastructure/config/
β
βββ domain/ # π’ CORE - Business Logic Pura
β βββ entities/ # Modelli di dominio (dataclass)
β β βββ __init__.py
β β βββ device.py # @dataclass Device, DeviceId
β β
β βββ ports/ # Interfacce/Contratti
β βββ inbound/ # Use Case interfaces (opzionale)
β β βββ device_service.py # Protocol/ABC per use cases
β βββ outbound/ # Repository interfaces
β βββ __init__.py
β βββ device_repository.py # ABC DeviceRepository
β
βββ application/ # π΅ USE CASES - Orchestrazione
β βββ __init__.py
β βββ use_cases/
β βββ __init__.py
β βββ get_device.py # GetDeviceUseCase
β βββ create_device.py # CreateDeviceUseCase
β βββ update_device.py # UpdateDeviceUseCase
β
βββ infrastructure/ # π ADAPTERS - Implementazioni Concrete
β βββ config/
β β βββ __init__.py
β β βββ settings.py # Pydantic Settings (β SPOSTARE config.py qui)
β β βββ database.py # SessionLocal, engine, get_db
β β βββ container.py # DI Container
β β
β βββ adapters/
β βββ inbound/ # Driving adapters (API, CLI, Workers)
β β βββ redis_consumer.py # Background workers
β β
β βββ outbound/ # Driven adapters (DB, External APIs)
β β βββ orm_models.py # SQLAlchemy models
β β βββ postgres_device_repository.py
β β βββ redis_cache.py
β β
β βββ clients/ # External service clients
β βββ billing_client.py
β
βββ routes/ # π£ HTTP ADAPTERS - FastAPI Routes
β βββ __init__.py
β βββ devices.py # APIRouter endpoints
β βββ health.py
β βββ schema.py # Route per schema/migrations
β
βββ tests/ # Test Suite
β βββ unit/ # Unit tests (mocked)
β βββ integration/ # Integration tests (DB)
β βββ conftest.py
β
βββ Dockerfile
βββ docker-compose.yml
βββ requirements.txt
βββ pytest.ini
βββ README.md
Cosa Rimuovere/Spostareβ
Nel tuo screenshot vedo file che andrebbero riorganizzati:
| File Attuale | Posizione Ideale | Azione |
|---|---|---|
config.py (root) | infrastructure/config/settings.py | Spostare |
routes/ (root) | OK β | Mantenere |
app.py (root) | OK β | Mantenere (entry point) |
Nota:
app.pyeDockerfilerimangono nella root perchΓ© sono entry point. Ma la configurazione deve stare ininfrastructure/perchΓ© Γ¨ un dettaglio implementativo.
I Layer Spiegatiβ
π’ Domain Layer (Centro)β
Il cuore dell'applicazione. Contiene:
- Entities: Oggetti di business puri (no dipendenze esterne)
- Ports: Interfacce/contratti che definiscono COSA fare, non COME
# domain/entities/device.py
from dataclasses import dataclass
from typing import Optional
@dataclass
class DeviceId:
value: int
@dataclass
class Device:
id: Optional[DeviceId]
name: str
protocol: str
unique_id: str
def to_dict(self) -> dict:
return {
"id": self.id.value if self.id else None,
"name": self.name,
"protocol": self.protocol,
}
# domain/ports/outbound/device_repository.py
from abc import ABC, abstractmethod
from domain.entities.device import Device, DeviceId
class DeviceRepository(ABC):
@abstractmethod
async def find_by_id(self, device_id: DeviceId) -> Optional[Device]:
pass
@abstractmethod
async def save(self, device: Device) -> Device:
pass
Regola d'Oro: Il Domain Layer NON importa mai nulla da infrastructure/, routes/, SQLAlchemy, FastAPI, ecc.
π΅ Application Layer (Use Cases)β
Contiene la logica di orchestrazione. Ogni Use Case:
- Riceve una Request (dataclass)
- Restituisce una Response (dataclass)
- Usa i Ports per interagire con l'esterno
# application/use_cases/get_device.py
from dataclasses import dataclass
from domain.entities.device import Device, DeviceId
from domain.ports.outbound.device_repository import DeviceRepository
@dataclass
class GetDeviceRequest:
device_id: int
user_id: int
@dataclass
class GetDeviceResponse:
device: Device
class GetDeviceUseCase:
def __init__(self, device_repository: DeviceRepository):
self._repo = device_repository
async def execute(self, request: GetDeviceRequest) -> GetDeviceResponse:
device = await self._repo.find_by_id(DeviceId(request.device_id))
if not device:
raise DeviceNotFoundError()
return GetDeviceResponse(device=device)
Vantaggi:
- Testabile con mock repository
- Non conosce HTTP, DB, Redis
- Riutilizzabile in CLI, workers, ecc.
π Infrastructure Layer (Adapters)β
Implementazioni concrete dei Ports:
# infrastructure/adapters/outbound/postgres_device_repository.py
from domain.ports.outbound.device_repository import DeviceRepository
from domain.entities.device import Device, DeviceId
from infrastructure.adapters.orm_models import DeviceModel
class PostgresDeviceRepository(DeviceRepository):
def __init__(self, session_factory):
self._session_factory = session_factory
async def find_by_id(self, device_id: DeviceId) -> Optional[Device]:
with self._session_factory() as session:
model = session.get(DeviceModel, device_id.value)
if model:
return self._to_entity(model)
return None
def _to_entity(self, model: DeviceModel) -> Device:
return Device(
id=DeviceId(model.id),
name=model.name,
protocol=model.protocol,
unique_id=model.unique_id,
)
Il DI Container assembla tutto:
# infrastructure/config/container.py
class Container:
def __init__(self, session_factory):
self._session_factory = session_factory
@property
def device_repository(self) -> DeviceRepository:
return PostgresDeviceRepository(self._session_factory)
@property
def get_device_use_case(self) -> GetDeviceUseCase:
return GetDeviceUseCase(device_repository=self.device_repository)
π£ Routes (HTTP Adapters)β
I routes FastAPI sono solo adattatori HTTP che:
- Validano input (Pydantic)
- Chiamano Use Cases
- Formattano output
# routes/devices.py
from fastapi import APIRouter, Depends, HTTPException
from infrastructure.config.container import get_container
from application.use_cases.get_device import GetDeviceRequest
router = APIRouter()
@router.get("/{device_id}")
async def get_device(device_id: int, current_user = Depends(get_current_user)):
container = get_container()
use_case = container.get_device_use_case
try:
response = await use_case.execute(
GetDeviceRequest(device_id=device_id, user_id=current_user.id)
)
return response.device.to_dict()
except DeviceNotFoundError:
raise HTTPException(404, "Device not found")
Dependency Ruleβ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β ROUTES (HTTP) β
β β conosce β β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β APPLICATION (Use Cases) β
β β conosce β β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β DOMAIN β
β entities, ports β
β β οΈ NON conosce nient'altro β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β INFRASTRUCTURE β
β implementa i ports definiti in domain β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Le dipendenze puntano verso l'interno (verso il Domain). Mai al contrario.
Vantaggi Praticiβ
1. Testing Semplificatoβ
# test_get_device.py
class MockDeviceRepository(DeviceRepository):
async def find_by_id(self, device_id):
return Device(id=DeviceId(1), name="Test", ...)
async def test_get_device_success():
use_case = GetDeviceUseCase(device_repository=MockDeviceRepository())
response = await use_case.execute(GetDeviceRequest(device_id=1, user_id=1))
assert response.device.name == "Test"
2. Cambio Database Trasparenteβ
Se domani vuoi passare da PostgreSQL a MongoDB, cambi solo l'adapter:
class MongoDeviceRepository(DeviceRepository):
# Nuova implementazione
pass
Il Use Case non cambia!
3. Riutilizzo Business Logicβ
Lo stesso Use Case puΓ² essere usato da:
- HTTP API (FastAPI routes)
- CLI commands
- Background workers
- GraphQL resolvers
Checklist Refactoringβ
-
config.pyspostato ininfrastructure/config/settings.py - Tutte le Entities sono
@dataclassindomain/entities/ - Repository definiti come
ABCindomain/ports/outbound/ - Ogni operazione ha un Use Case in
application/use_cases/ - ORM models solo in
infrastructure/adapters/orm_models.py - Routes chiamano Use Cases, non repository direttamente
- DI Container in
infrastructure/config/container.py