Dependency Direction

Clarifier la direction des dépendances dans une architecture hexagonale pour éviter que l’infrastructure remonte dans le domaine.

← Retour au concept parent

Le problème

Beaucoup d’équipes comprennent l’idée des ports et adapters, mais le design se dégrade quand même pour une raison simple :

les dépendances partent dans le mauvais sens.

On voit souvent ce genre de situation :

  • le domaine importe des types HTTP
  • un use case dépend directement d’un repository ORM concret
  • les objets métier connaissent le schéma SQL
  • la logique métier manipule des DTO d’API externe
  • un service applicatif dépend d’un SDK Stripe, AWS ou d’un client Kafka

À ce moment-là, l’architecture hexagonale existe peut-être dans les dossiers, mais plus dans le code.

Le vrai sujet n’est pas seulement “avoir des couches”. Le vrai sujet est :

qui a le droit de dépendre de quoi.

L’idée simple

Dans une architecture hexagonale, les dépendances doivent pointer vers le cœur du système, pas l’inverse.

Autrement dit :

  • l’infrastructure dépend de l’application ou du domaine
  • l’application dépend du domaine
  • le domaine ne dépend pas de l’infrastructure

La base de données, le framework, le transport HTTP ou les services externes doivent se brancher sur le cœur. Le cœur ne doit pas être construit autour d’eux.

La bonne question à se poser n’est donc pas :

“Dans quel dossier je mets cette classe ?”

La bonne question est :

“Dans quel sens cette dépendance part-elle ?”

Comment ça fonctionne

La direction des dépendances est une règle simple à énoncer et difficile à garder si on ne la rend pas explicite.

1. Le domaine doit rester stable

Le domaine porte ce qui change le moins pour de mauvaises raisons :

  • les règles métier
  • les invariants
  • les concepts métier
  • les comportements importants

Il peut évoluer, bien sûr, mais il ne devrait pas changer parce que :

  • on remplace PostgreSQL par DynamoDB
  • on passe d’Express à Fastify
  • on change de provider de paiement
  • on ajoute Kafka
  • on change un format HTTP

Si le domaine change pour des raisons purement techniques, la dépendance va déjà dans le mauvais sens.

2. L’infrastructure doit s’adapter

L’infrastructure contient les détails volatils :

  • base de données
  • framework web
  • bus de messages
  • clients d’API
  • système de fichiers
  • cache
  • authentification externe

Ces éléments changent plus souvent et varient selon le contexte du projet.

Ils doivent donc dépendre du cœur pour l’utiliser ou l’implémenter.

Par exemple :

  • un controller HTTP dépend d’un use case
  • un repository PostgreSQL implémente un port défini côté application ou domaine
  • un client Stripe implémente un port de paiement

3. Les ports servent à contrôler la direction

La direction des dépendances ne repose pas sur la bonne volonté. Elle repose sur des contrats placés au bon endroit.

Exemple :

  • le domaine ou l’application déclare PaymentGateway
  • l’infrastructure fournit StripePaymentGateway

Le besoin vient du cœur. L’implémentation vient du bord.

C’est ce placement qui inverse le couplage attendu par défaut.

Schéma

Direction des dépendances

Lecture utile du schéma :

  • les détails techniques dépendent de la couche application
  • la couche application dépend du domaine
  • le domaine reste au centre
  • aucune flèche ne remonte du domaine vers l’infrastructure

Exemple concret

Prenons un use case simple : confirmer une réservation.

Le système doit :

  • charger une réservation
  • vérifier qu’elle peut être confirmée
  • enregistrer le nouvel état
  • envoyer une confirmation

Mauvaise direction de dépendance

import { Request } from "express";
import { PrismaClient } from "@prisma/client";
import { Resend } from "resend";

const prisma = new PrismaClient();
const resend = new Resend(process.env.RESEND_API_KEY);

export class ConfirmBookingService {
  async execute(req: Request): Promise<void> {
    const booking = await prisma.booking.findUnique({
      where: { id: req.body.bookingId },
    });

    if (!booking) {
      throw new Error("Booking not found");
    }

    if (booking.status !== "pending") {
      throw new Error("Booking cannot be confirmed");
    }

    await prisma.booking.update({
      where: { id: booking.id },
      data: { status: "confirmed" },
    });

    await resend.emails.send({
      to: booking.email,
      subject: "Booking confirmed",
      html: "<p>Your booking is confirmed.</p>",
    });
  }
}

Ce code mélange tout :

  • transport HTTP
  • accès base
  • logique métier
  • service d’email externe

Le résultat :

  • difficile à tester
  • difficile à faire évoluer
  • dépendant de détails techniques partout

Bonne direction de dépendance

Ports

export interface ConfirmBookingUseCase {
  execute(command: ConfirmBookingCommand): Promise<void>;
}

export type ConfirmBookingCommand = {
  bookingId: string;
};

export interface BookingRepository {
  findById(id: string): Promise<Booking | null>;
  save(booking: Booking): Promise<void>;
}

export interface NotificationGateway {
  sendBookingConfirmed(booking: Booking): Promise<void>;
}

Use case

export class ConfirmBookingService implements ConfirmBookingUseCase {
  constructor(
    private readonly bookingRepository: BookingRepository,
    private readonly notificationGateway: NotificationGateway
  ) {}

  async execute(command: ConfirmBookingCommand): Promise<void> {
    const booking = await this.bookingRepository.findById(command.bookingId);

    if (!booking) {
      throw new Error("Booking not found");
    }

    booking.confirm();

    await this.bookingRepository.save(booking);
    await this.notificationGateway.sendBookingConfirmed(booking);
  }
}

Adapter HTTP

import type { Request, Response } from "express";

export class ConfirmBookingController {
  constructor(
    private readonly confirmBookingUseCase: ConfirmBookingUseCase
  ) {}

  async handle(req: Request, res: Response): Promise<void> {
    await this.confirmBookingUseCase.execute({
      bookingId: req.body.bookingId,
    });

    res.status(204).send();
  }
}

Adapter PostgreSQL

export class PgBookingRepository implements BookingRepository {
  async findById(id: string): Promise<Booking | null> {
    // SQL ou ORM + mapping vers Booking
    return null;
  }

  async save(booking: Booking): Promise<void> {
    // Persistance
  }
}

Adapter email

export class ResendNotificationGateway implements NotificationGateway {
  async sendBookingConfirmed(booking: Booking): Promise<void> {
    // Appel provider email
  }
}

Ici, la direction est correcte :

  • le controller dépend du use case
  • les adapters techniques implémentent les ports
  • le cœur ne connaît pas Express, Prisma ni Resend

Comment vérifier la direction des dépendances

En pratique, trois questions suffisent souvent.

Cette classe connaît-elle un détail technique inutile ?

Exemples de signaux faibles :

  • Request, Response
  • entités ORM
  • clients SDK
  • modèles SQL
  • payloads bruts d’un broker

Si oui, demande-toi si cette connaissance doit vraiment exister à cet endroit.

Cette dépendance existe-t-elle parce que le métier en a besoin ou parce qu’une technologie est là ?

Exemple :

  • “le métier a besoin d’envoyer une confirmation” → bon signal
  • “on a mis le SDK SendGrid ici parce qu’il fallait bien l’appeler quelque part” → mauvais signal

Le cœur exprime un besoin. Le bord choisit la technologie.

Est-ce que je peux tester cette logique sans framework ni I/O réelle ?

Si la réponse est non, il y a souvent un problème de direction de dépendance.

Exemple de structure saine

src/
  booking/
    domain/
      booking.ts
    application/
      confirm-booking/
        confirm-booking.use-case.ts
        confirm-booking.command.ts
        booking-repository.port.ts
        notification-gateway.port.ts
        confirm-booking.service.ts
    infrastructure/
      http/
        confirm-booking.controller.ts
      persistence/
        pg-booking-repository.ts
      notification/
        resend-notification-gateway.ts

Ce rangement n’est pas magique à lui seul. Mais il devient cohérent si les imports suivent réellement cette direction :

  • infrastructure -> application
  • application -> domain

et jamais l’inverse.

Points importants

  • Le sujet n’est pas seulement le découpage en couches.
  • Le sujet est la direction réelle des imports et des dépendances.
  • Le domaine ne doit pas dépendre de détails volatils.
  • Les ports servent à faire remonter les besoins du cœur sans faire remonter la technique.
  • Une architecture visuellement propre peut être fausse si les dépendances vont dans le mauvais sens.
  • La testabilité est souvent une conséquence directe d’une bonne direction de dépendance.

Erreurs fréquentes

Croire qu’un dossier domain/ suffit

Mettre du code dans un dossier domain n’isole rien si ce code importe toujours :

  • des entités ORM
  • des types HTTP
  • des SDK externes

Le nom du dossier ne protège pas l’architecture.

Confondre injection de dépendances et bonne direction

Injecter une classe concrète ne règle pas le problème si cette classe tire déjà le cœur vers l’infrastructure.

L’injection aide. Elle ne remplace pas le bon placement des contrats.

Laisser les types techniques traverser les couches

Une requête HTTP transformée trop tard, ou un DTO Stripe remonté jusqu’au domaine, sont des fuites de dépendances.

La traduction doit se faire au bord.

Créer des ports trop techniques

Un port comme PrismaBookingRepository ou StripeApiClient révèle déjà un problème.

Un port doit exprimer un besoin du cœur, pas le nom d’une technologie.

Mettre la logique métier dans les implémentations concrètes

Quand le repository concret ou le client externe contient des règles métier, le cœur perd la main.

Les implémentations concrètes servent à brancher la technique. La décision métier doit rester ailleurs.

Conclusion

La direction des dépendances est la règle qui tient toute l’architecture hexagonale.

Quand les dépendances pointent vers le cœur :

  • le métier reste lisible
  • les détails techniques restent remplaçables
  • les tests deviennent plus simples
  • le système vieillit mieux

Le réflexe le plus utile est simple :

les détails dépendent du cœur, jamais l’inverse.