Hexagonale, Clean Architecture, DDD : comment les distinguer pour de bon

Sur une mission récente, un développeur m'a demandé si son projet était « en hexagonal ou en DDD ». La question n'a pas de sens telle quelle : ce sont deux réponses à deux problèmes différents, et rien n'empêche de les combiner. Cette confusion revient assez souvent pour mériter d'être démêlée une bonne fois, ce que j'ai fini par formaliser dans archi3, un site pédagogique bilingue avec un article par approche et un glossaire recherchable.
Le problème que chacune résout#
| Approche | Problème résolu | Concept central |
|---|---|---|
| Architecture hexagonale | Isoler le métier des détails techniques (base de données, framework, API externe) | Ports et adapters |
| Clean Architecture | Structurer les dépendances pour qu'elles pointent toujours vers l'intérieur | La règle de dépendance, quatre cercles |
| Domain-Driven Design | Modéliser un domaine métier complexe avec un vocabulaire partagé | Le langage ubiquitaire, les agrégats |
Aucune des trois ne dit comment nommer vos classes ni comment organiser vos dossiers. Ce sont des règles de dépendance et de modélisation, pas des conventions de nommage, ce qui explique une bonne partie des malentendus : deux projets « hexagonaux » peuvent avoir une arborescence complètement différente et être tous les deux corrects.
Architecture hexagonale : ports et adapters#
L'idée tient en une phrase : le métier définit des interfaces (les ports), et tout ce qui est technique les implémente (les adapters). Une base de données, une API REST, une file de messages : ce sont des adapters interchangeables, jamais des dépendances du domaine.
// Port : défini par le domaine, sans dépendance technique
interface CollateralRepository {
fun findByTradeId(tradeId: TradeId): Collateral?
}
// Adapter : implémente le port, connaît la technologie
class PostgresCollateralRepository(
private val jdbcTemplate: JdbcTemplate,
) : CollateralRepository {
override fun findByTradeId(tradeId: TradeId): Collateral? {
// requête SQL, mapping, etc.
TODO()
}
}Le piège le plus fréquent : définir un port qui expose déjà des détails techniques (un type ResultSet, une pagination Spring Data) dans sa signature. À ce moment-là, le port n'isole plus rien, il déplace juste le problème d'un fichier à l'autre.
Clean Architecture : la règle de dépendance#
Clean Architecture formalise la même intuition sous une forme différente : quatre cercles concentriques (entités, cas d'usage, adapters d'interface, frameworks), avec une seule règle absolue, une flèche de dépendance qui ne pointe jamais vers l'extérieur. Le cercle du milieu ne sait rien de celui du dessus.
En pratique, sur un projet Kotlin/Spring Boot, ça se traduit par : le module domain ne dépend d'aucune annotation Spring, d'aucune classe JPA, d'aucun @RestController. Si votre IDE propose d'importer org.springframework.* dans une classe de domaine, c'est un signal que la règle vient d'être violée.
Domain-Driven Design : modéliser, pas seulement isoler#
Là où hexagonale et Clean Architecture parlent de dépendances, DDD parle de modélisation. Le langage ubiquitaire (le vocabulaire métier partagé entre développeurs et experts métier) et les agrégats (des frontières de cohérence transactionnelle) répondent à une question différente : comment représenter fidèlement un domaine complexe dans le code, pas seulement où placer les couches.
// Un agrégat DDD : la cohérence de "Trade" est garantie
// par la racine, jamais en accédant à Collateral directement
class Trade private constructor(
val id: TradeId,
private val collaterals: MutableList<Collateral>,
) {
fun postCollateral(collateral: Collateral) {
require(collateral.currency == this.currency) {
"Devise du collatéral incompatible avec le trade"
}
collaterals.add(collateral)
}
}C'est la raison pour laquelle DDD se combine si naturellement avec hexagonale : DDD dit comment modéliser l'intérieur du cercle métier, hexagonale dit comment protéger cet intérieur du reste. Ce ne sont pas deux écoles rivales, ce sont deux réponses à deux questions qui se posent l'une après l'autre.
Le vrai piège : les traiter comme mutuellement exclusifs#
La confusion la plus coûteuse n'est pas de mélanger le vocabulaire, c'est de croire qu'il faut choisir. Sur MissionMatch, le domaine est modélisé avec des agrégats et un langage ubiquitaire DDD, structuré selon une architecture hexagonale, avec une séparation domaine/application/infrastructure qui respecte la règle de dépendance de Clean Architecture. Les trois cohabitent parce qu'elles répondent à des questions différentes : comment modéliser, comment structurer les dépendances, comment isoler la technique.
La question à se poser n'est donc jamais « lequel des trois ». C'est : où est le vocabulaire métier partagé (DDD), où sont mes frontières de dépendance (hexagonale), et est-ce que ces frontières respectent une direction cohérente (Clean Architecture). Répondre aux trois en même temps n'est pas une contradiction, c'est le cas normal.


