Application Layer vs Domain Layer

Distinguer orchestration et logique métier pour savoir quoi mettre dans la couche application et quoi garder dans le domaine.

← Retour au concept parent

Le problème

Dans beaucoup de projets, la frontière entre couche application et couche domaine est floue.

On voit souvent ce genre de code :

  • un service applicatif qui contient toute la logique métier
  • une entité vide qui ne fait que stocker des données
  • un use case qui gère à la fois les règles métier, les appels externes et le mapping technique
  • un domain service créé juste pour sortir du code d’un controller
  • des validations métier dispersées entre DTO, handlers, services et repositories

Le résultat est presque toujours le même :

  • on ne sait plus où mettre une règle
  • la logique métier se dilue
  • le domaine devient anémique
  • l’application devient un gros script procédural
  • la maintenance devient plus coûteuse qu’elle ne devrait

Le problème n’est pas d’avoir deux couches. Le problème est de ne pas savoir ce que chacune doit porter.

L’idée simple

La distinction utile est simple :

  • la couche application orchestre
  • la couche domaine décide

La couche application coordonne l’exécution d’un cas d’usage :

  • reçoit une commande
  • charge les données nécessaires
  • appelle le domaine
  • déclenche les effets de bord nécessaires
  • retourne un résultat

La couche domaine porte la logique métier :

  • règles
  • invariants
  • comportements métier
  • décisions importantes

Autrement dit :

  • l’application dit dans quel ordre
  • le domaine dit ce qui est valide

Comment ça fonctionne

La séparation devient claire quand on regarde les responsabilités réelles.

1. La couche application orchestre un cas d’usage

La couche application représente ce que le système sait faire.

Exemples :

  • créer un abonnement
  • confirmer une réservation
  • annuler une commande
  • générer une facture

Elle s’occupe généralement de :

  • recevoir une commande d’entrée
  • appeler les bons objets métier
  • charger et sauvegarder via des ports
  • gérer les transactions au bon niveau
  • déclencher des notifications, événements ou intégrations

Elle ne devrait pas contenir les règles métier centrales.

2. La couche domaine porte les règles métier

La couche domaine représente les concepts et comportements du métier.

Exemples :

  • une commande ne peut pas être confirmée si elle est vide
  • une réservation ne peut pas être confirmée deux fois
  • une remise ne peut pas dépasser le sous-total
  • un abonnement suspendu ne peut pas être réactivé dans certains cas

Le domaine doit pouvoir exprimer ces règles sans dépendre :

  • d’un framework
  • d’un transport HTTP
  • d’un ORM
  • d’un provider externe

C’est lui qui protège la cohérence métier.

3. L’application coordonne, le domaine tranche

Une bonne lecture est la suivante :

  • application : “je veux exécuter ce scénario”
  • domaine : “voici ce qui est autorisé ou interdit”

Cette distinction évite deux erreurs :

  • un domaine vide
  • une couche application transformée en monolithe procédural

Schéma

Couche application vs couche domaine

Lecture utile :

  • l’application reçoit le scénario
  • le domaine applique les règles métier
  • l’application coordonne les accès externes
  • l’infrastructure reste au bord

Exemple concret

Prenons un cas simple : confirmer une réservation.

Les règles métier sont :

  • une réservation déjà confirmée ne peut pas être reconfirmée
  • une réservation annulée ne peut pas être confirmée
  • une réservation doit avoir au moins un créneau valide

Le système doit aussi :

  • charger la réservation
  • enregistrer le nouvel état
  • envoyer une notification

Mauvais découpage

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

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

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

    if (booking.status === "confirmed") {
      throw new Error("Booking is already confirmed");
    }

    if (booking.status === "cancelled") {
      throw new Error("Cancelled booking cannot be confirmed");
    }

    if (booking.slots.length === 0) {
      throw new Error("Booking must have at least one slot");
    }

    booking.status = "confirmed";

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

Ce code fonctionne, mais la logique métier vit dans la couche application. Le domaine n’est qu’un sac de données.

Meilleur découpage

Domaine

export class Booking {
  private status: "pending" | "confirmed" | "cancelled";
  private readonly slots: string[];

  constructor(params: {
    status: "pending" | "confirmed" | "cancelled";
    slots: string[];
  }) {
    this.status = params.status;
    this.slots = params.slots;
  }

  confirm(): void {
    if (this.status === "confirmed") {
      throw new Error("Booking is already confirmed");
    }

    if (this.status === "cancelled") {
      throw new Error("Cancelled booking cannot be confirmed");
    }

    if (this.slots.length === 0) {
      throw new Error("Booking must have at least one slot");
    }

    this.status = "confirmed";
  }
}

Application

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

export type ConfirmBookingCommand = {
  bookingId: string;
};

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);
  }
}

Ici, la différence est nette :

  • l’application orchestre le scénario
  • le domaine décide si la confirmation est autorisée

C’est beaucoup plus robuste.

Ce qui va dans la couche application

En pratique, la couche application porte souvent :

  • les use cases
  • les commandes et résultats
  • l’orchestration
  • la coordination entre ports
  • la gestion de transaction
  • le déclenchement d’effets de bord
  • l’autorisation au niveau scénario
  • parfois la validation d’entrée simple

Exemples de bonnes responsabilités :

  • charger un aggregate
  • appeler order.confirm()
  • sauvegarder l’aggregate
  • publier un événement
  • appeler un port de notification

La couche application parle en scénarios.

Ce qui va dans la couche domaine

Le domaine porte ce qui exprime réellement le métier :

  • entités
  • value objects
  • aggregates
  • domain services quand ils sont justifiés
  • règles métier
  • invariants
  • transitions d’état
  • calculs métier

Exemples :

  • refuser une commande vide
  • recalculer un total
  • empêcher une transition interdite
  • vérifier une contrainte métier de cohérence

Le domaine parle en décisions et comportements métier.

Ce qui ne doit pas être dans le domaine

Le domaine ne devrait pas porter :

  • des détails HTTP
  • des objets ORM
  • des DTO de transport
  • du code de persistence
  • des clients de provider externe
  • du mapping technique
  • des retries réseau
  • de la logique purement applicative de scénario

Exemple classique : “envoyer un email de confirmation” n’est généralement pas une règle métier centrale. C’est souvent une conséquence applicative.

Ce qui ne doit pas être dans la couche application

La couche application ne devrait pas devenir :

  • un gros script avec toutes les règles métier
  • une succession de if métier dispersés
  • une couche qui décide des invariants à la place du modèle
  • un substitut à un domaine mal conçu

Si chaque use case contient toute l’intelligence du métier, le domaine perd tout son intérêt.

Exemple de structure simple

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

Cette structure reste saine si la frontière est respectée :

  • application orchestre
  • domain décide
  • infrastructure branche la technique

Points importants

  • La couche application exécute un scénario.
  • La couche domaine porte les règles métier.
  • L’application coordonne, le domaine protège la cohérence.
  • Un domaine anémique est souvent le signe d’une couche application trop lourde.
  • Tout ne doit pas aller dans le domaine, mais les règles métier importantes oui.
  • Les effets de bord et les intégrations externes restent généralement hors du domaine.

Erreurs fréquentes

Mettre toute la logique dans les use cases

C’est l’erreur la plus fréquente.

Les use cases deviennent de gros scripts procéduraux et le domaine ne sert plus qu’à stocker des champs.

Faire un domaine purement data

Une entité sans comportement réel n’aide pas à protéger le métier.

Si tout est modifiable de l’extérieur, les invariants finissent dispersés ailleurs.

Déplacer dans le domaine des préoccupations applicatives

Par exemple :

  • envoyer un email
  • publier directement sur Kafka
  • manipuler des DTO HTTP
  • gérer une transaction SQL

Le domaine ne doit pas absorber des responsabilités techniques ou de scénario qui n’y ont rien à faire.

Créer des domain services pour tout

Un domain service est utile dans certains cas, mais il ne doit pas devenir un refuge pour tout le code qu’on ne sait pas placer.

Souvent, la bonne place est soit :

  • dans une entité ou un aggregate
  • dans un use case applicatif

Confondre validation d’entrée et validation métier

Vérifier qu’un champ est présent ou bien formé est souvent une validation d’entrée. Vérifier qu’une commande est confirmable est une validation métier.

Les deux n’ont pas la même place.

Conclusion

La différence entre couche application et couche domaine est simple quand on la ramène à leur rôle réel :

  • l’application orchestre le scénario
  • le domaine protège la logique métier

Le réflexe le plus utile est simple :

si le code décide ce qui est métierment autorisé, il appartient probablement au domaine.