Pourquoi l architecture hexagonale change vos refactors ?
L architecture hexagonale (Ports & Adapters) sert a proteger votre logique metier contre les changements techniques. Le framework, la base SQL, Redis ou un provider externe deviennent des details remplacables.
Le probleme que l architecture hexagonale resout
Quand le metier depend directement de Prisma, Express ou Stripe, chaque changement technique cree un impact fort sur le coeur applicatif.
- Difficile de tester le metier sans DB ou API externe.
- Refactor long car tout est couple au framework.
- Migration technique risquee (ORM, transport, provider).
1// BAD: couplage fort, le use case depend directement de Prisma et Stripe2
3import { prisma } from '@/lib/prisma';4import Stripe from 'stripe';5
6const stripe = new Stripe(process.env.STRIPE_SECRET!);7
8export async function createOrder(amountCents: number, customerId: string) {9 if (amountCents < 100) throw new Error('Montant minimum: 1 EUR');10
11 const payment = await stripe.paymentIntents.create({12 amount: amountCents,13 currency: 'eur',14 });15
16 return prisma.order.create({17 data: {18 customerId,19 amountCents,20 status: payment.status === 'succeeded' ? 'PAID' : 'PENDING',21 paymentId: payment.id,22 },23 });24}25
26// Si Stripe ou Prisma changent, la logique metier doit etre reecrite.Quelle est la regle de dependance a respecter ?
Regle de dependance
Le code metier (centre) ne connait pas les details techniques (exterieur). Il depend uniquement d abstractions.
- Les adapters importent le domaine.
- Le domaine n importe jamais les adapters.
- Les ports sont definis dans le coeur, implementes a l exterieur.
Schema direction des dependances
Infrastructure
- - HTTP
- - CLI
- - SQL
- - Queue
- - Webhook
Adapters
- - Controllers
- - Repositories
- - Gateways
Application
- - Use cases
- - DTO
- - Ports
Domain
- - Entities
- - Value Objects
- - Rules
Lecture de droite a gauche: chaque couche peut dependre de la couche interne, jamais de la couche externe.
1// OK Le domaine definit les ports (interfaces)2
3export interface PaymentPort {4 charge(input: {5 amountCents: number;6 currency: string;7 reference: string;8 }): Promise<{9 provider: string;10 transactionId: string;11 authorized: true;12 }>;13}14
15export interface OrderRepositoryPort {16 save(order: OrderEntity): Promise<PersistedOrder>;17}18
19// Use case: depend uniquement des interfaces20export async function createOrderUseCase(21 command: CreateOrderCommand,22 deps: {23 paymentPort: PaymentPort;24 orderRepository: OrderRepositoryPort;25 }26) {27 // logique metier pure28}29
30// Les adapters (Stripe, Postgres, Mock...) se branchent via deps.A quoi ressemble le schema Ports & Adapters ?
Le schema ci dessous montre la separation entre le coeur metier et les details techniques. C est cette frontiere qui permet de remplacer un adapter sans toucher aux regles metier.
Adapters entrants
- - HTTP Controller
- - CLI Command
- - Queue Consumer
Hexagone metier
- - Entites
- - Use cases
- - Ports (interfaces)
Adapters sortants
- - SQL Repository
- - Redis Cache
- - Payment Provider
Regle cle: les dependances pointent vers le centre (le metier). Les adapters peuvent changer sans impacter les regles metier.
Ports entrants vs ports sortants
Les ports entrants exposent les cas d usage. Les ports sortants expriment les besoins d infrastructure du domaine.
- Port entrant : interface d entree d un use case (ex: CreateOrder).
- Port sortant : contrat necessaire au domaine (ex: PaymentPort, OrderRepositoryPort).
- Adapter : implementation concrete d un port (ex: StripeAdapter, PostgresRepository).
Comment circule une requete du controller au domaine ?
Schema de flux d execution
1. Adapter entrant
Controller/API traduit la requete en commande metier.
2. Use case
Le coeur applique les regles et orchestre les ports.
3. Ports sortants
Le domaine appelle des interfaces abstraites.
4. Adapters sortants
DB, provider externe, cache implementent les ports.
1// Controller (adapter entrant)2export async function createOrderAction(formData: FormData) {3 const amountCents = Number(formData.get('amountCents'));4
5 const result = await createOrderUseCase(6 {7 customerId: 'customer_42',8 amountCents,9 currency: 'EUR',10 },11 {12 paymentPort: new StripePaymentAdapter(stripeClient),13 orderRepository: new PostgresOrderRepository(prisma),14 }15 );16
17 return result;18}19
20// Le use case ne depend pas de Next.js, Prisma ou Stripe.Comment structurer un projet Next.js en hexagonal ?
1src/2 domain/3 orders/4 entities/order.ts5 ports/payment-port.ts6 ports/order-repository-port.ts7 use-cases/create-order.ts8
9 infrastructure/10 orders/11 adapters/inbound/create-order-action.ts12 adapters/outbound/stripe-payment-adapter.ts13 adapters/outbound/postgres-order-repository.ts14
15 app/16 (dashboard)/orders/page.tsx17
18// Regle de lecture:19// - domain/ ne depend de rien d externe20// - infrastructure/ depend du domain21// - app/ assemble les dependenciesComment brancher les dependances dans Next.js
Composez les adapters au bord de l application (server action, route handler, job worker).
- Le domaine reste framework-agnostique.
- Les adapters sont instancies a l entree (composition root).
- Un changement de provider devient un changement localise.
Comment migrer sans big bang ?
Pas besoin de tout recrire. Une migration progressive limite le risque et vous donne des gains rapidement.
1// Plan de migration en 4 etapes2
3// 1) Choisir un use case critique4// Exemple: create-order5
6// 2) Extraire les ports sortants dans le domaine7// PaymentPort, OrderRepositoryPort8
9// 3) Creer des adapters techniques10// StripePaymentAdapter, PostgresOrderRepository11
12// 4) Assembler dans un controller/server action13// createOrderUseCase(command, { paymentPort, orderRepository })14
15// Ensuite repeter use case par use case.16// La migration devient incrementale plutot que big bang.Pourquoi les tests deviennent plus simples et plus rapides ?
Le gain le plus visible arrive cote tests: vous pouvez valider la logique metier sans demarrer Postgres, Redis ou Stripe.
1import { describe, expect, it } from 'vitest';2import { createOrderUseCase } from './create-order';3
4describe('createOrderUseCase', () => {5 it('cree une commande payee', async () => {6 const fakePaymentPort = {7 charge: async () => ({8 provider: 'FakePay',9 transactionId: 'tx_test',10 authorized: true as const,11 }),12 };13
14 const fakeRepository = {15 save: async (order: any) => ({16 ...order,17 storage: 'InMemory',18 persistedAt: new Date().toISOString(),19 }),20 };21
22 const result = await createOrderUseCase(23 {24 customerId: 'c_1',25 amountCents: 2400,26 currency: 'EUR',27 },28 {29 paymentPort: fakePaymentPort,30 orderRepository: fakeRepository,31 }32 );33
34 expect(result.order.status).toBe('PAID');35 expect(result.payment.provider).toBe('FakePay');36 });37});Strategie de test recommandee
Concentrez vos tests sur le domaine, puis testez les adapters separement avec leurs dependances techniques.
- Tests unitaires: use cases + fakes (rapides et stables).
- Tests integration adapters: DB, API, cache, providers.
- Tests E2E: parcours critiques seulement.
Quels pieges eviter avec l architecture hexagonale ?
- Implementation initiale rapide
- Tests fragiles
- Refactor risquee
- Migration technique couteuse
- •Prototype court terme
- Tests metier rapides
- Adapters interchangeables
- Code durable sur la longueur
- Un peu plus de setup au debut
- •Produit en croissance
- •Equipe multi-dev
- •Contexte long terme
Couplage direct | Hexagonale |
|---|---|
Le metier importe Prisma/Stripe/HTTP et ne peut pas tourner seul. | Le metier ne depend que des ports. Les adapters implementent les details. |
Avantages
| Avantages
|
Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
|
Erreurs frequentes
Hexagonal ne veut pas dire multiplier les couches sans but. Restez pragmatique.
- Definir des ports pour tout, meme quand inutile.
- Mettre la logique metier dans les adapters entrants.
- Cacher la complexite au lieu de la reduire.
Demo: et si on remplacait les adapters en direct ?
INTERACTIFCette demo montre exactement le principe des ports et adapters: vous changez l implementation technique, mais le use case metier reste identique.
Sandbox Hexagonal Architecture
interactifAdapter entrant API HTTP (JSON request -> command)
Provider externe classique (latence reseau)
Adapter ultra rapide pour tests et preview
Adapter sortant simple (application log)
Timeline de traitement
Aucune execution pour le moment.
Resultat
Executez la demo pour voir le resultat du use case.
Lancez des scenarios complets avec timeline et resultat detaille.
Felicitations !
Vous avez termine ce guide.