Architecture applicative¶
Organiser le code en couches claires pour que les règles métier ne dépendent jamais des détails techniques.
Le problème central¶
Sans architecture explicite, la logique métier finit toujours par se retrouver melangee aux appels HTTP, aux requêtes SQL et aux détails d'infrastructure. Résultat : tester un calcul de TVA nécessité une base de données réelle.
L'objectif de toutes les architectures ci-dessous est le même :
Les règles métier ne dépendent pas de la technologie. La technologie dépend des règles métier.
Vue d'ensemble comparative¶
graph LR
subgraph Layered["Layered (traditionnel)"]
direction TB
L1["Presentation"] --> L2["Business"]
L2 --> L3["Data Access"]
end
subgraph Clean["Clean / Hexagonal"]
direction TB
C1["Frameworks & Drivers"]
C2["Interface Adapters"]
C3["Use Cases"]
C4["Entities"]
C1 --> C2 --> C3 --> C4
end Clean Architecture¶
Robert Martin (Uncle Bob) propose quatre cercles concentriques. La règle absolue : les dépendances ne pointent que vers l'intérieur.
graph TD
FW["Frameworks & Drivers<br/>(Web, DB, UI)"]
IA["Interface Adapters<br/>(Controllers)"]
UC["Use Cases<br/>(App logic)"]
EN["Entities<br/>(Domain)"]
FW --> IA --> UC --> EN
style EN fill:#2d6a4f,stroke:#1b4332,color:#fff
style UC fill:#40916c,stroke:#2d6a4f,color:#fff
style IA fill:#74c69d,stroke:#40916c,color:#000
style FW fill:#b7e4c7,stroke:#74c69d,color:#000 Structure projet typique en Python :
src/
domain/
entities/
order.py # Order, OrderItem — pas d'import externe
value_objects/
money.py
application/
use_cases/
create_order.py # orchestre domain + ports
ports/
order_repository.py # interface abstraite
infrastructure/
persistence/
postgres_order_repo.py # implemente le port
web/
order_controller.py # FastAPI / Flask
# domain/entities/order.py — zero dependance externe
class Order:
def __init__(self, customer_id: str):
self.customer_id = customer_id
self.items: list = []
self.status = "draft"
def add_item(self, product_id: str, qty: int, price: float):
self.items.append({"product_id": product_id, "qty": qty, "price": price})
def total(self) -> float:
return sum(i["qty"] * i["price"] for i in self.items)
def confirm(self):
if not self.items:
raise ValueError("Commande vide")
self.status = "confirmed"
Architecture Hexagonale (Ports & Adapters)¶
Proposee par Alistair Cockburn, elle nomme explicitement les points d'entree et de sortie.
graph LR
subgraph Core["Application Core"]
UC["Use Cases"]
DOM["Domain"]
end
HTTP["HTTP Adapter"] -->|driving port| UC
CLI["CLI Adapter"] -->|driving port| UC
UC -->|driven port| DB["DB Adapter"]
UC -->|driven port| SMTP["SMTP Adapter"] - Ports primaires (driving) : ce que l'application expose (API REST, CLI, message queue)
- Ports secondaires (driven) : ce dont l'application a besoin (base de données, emails, services tiers)
- Adapters : implémentations concrètes des ports
# application/ports/order_repository.py
from abc import ABC, abstractmethod
from domain.entities.order import Order
class OrderRepository(ABC):
@abstractmethod
def save(self, order: Order) -> None: pass
@abstractmethod
def find_by_id(self, order_id: str) -> Order | None: pass
# infrastructure/persistence/postgres_order_repo.py
class PostgresOrderRepository(OrderRepository):
def __init__(self, session):
self._session = session
def save(self, order: Order) -> None:
# mapping domain -> ORM
pass
def find_by_id(self, order_id: str) -> Order | None:
pass
# infrastructure/persistence/in_memory_order_repo.py — pour les tests
class InMemoryOrderRepository(OrderRepository):
def __init__(self):
self._store: dict[str, Order] = {}
def save(self, order: Order) -> None:
self._store[id(order)] = order
def find_by_id(self, order_id: str) -> Order | None:
return self._store.get(order_id)
L'adapteur in-memory est votre meilleur ami
Implémenter un port en mémoire prend 10 lignes et rend vos tests de use cases 100x plus rapides que des tests avec base de données réelle.
Onion Architecture¶
Variante de Clean Architecture par Jeffrey Palermo. Les couches sont les mêmes mais la terminologie différé :
| Couche | Contenu |
|---|---|
| Domain Model | Entités, Value Objects, logique pure |
| Domain Services | Services operant sur plusieurs entités |
| Application | Use cases, orchestration, ports |
| Infrastructure | DB, HTTP, fichiers, services externes |
La règle reste la même : les imports vont de l'extérieur vers l'intérieur, jamais l'inverse.
Monolithe modulaire vs microservices¶
Avant de passer aux microservices, essayez le monolithe modulaire :
src/
modules/
orders/
domain/
application/
infrastructure/
billing/
domain/
application/
infrastructure/
catalog/
...
shared/
events/ # communication inter-modules par evenements
| Critère | Monolithe modulaire | Microservices |
|---|---|---|
| Complexité initiale | Faible | Élevée (infra, réseau, observabilité) |
| Déploiement | Simple | Complexe (orchestration) |
| Isolation des pannes | Partielle | Forte |
| Équipe | 1-10 personnes | Plusieurs équipes autonomes |
| Quand migrer | Quand les modules divergent | Jamais si le monolithe suffit |
Commencez modulaire, pas micro
La majorité des startups qui ont commence avec des microservices les ont regroupes en monolithe après 18 mois. Construisez des modules bien isoles d'abord — la migration vers des services indépendants devient triviale si les frontieres sont propres.