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
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
ifmé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 :
applicationorchestredomaindécideinfrastructurebranche 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.