Architecture hexagonale en Kotlin : quand le code respire librement
\n\nImaginez que vous construisez une maison. Vous pouvez directement couler les fondations, élever les murs et poser les fenêtres. Ou vous pouvez d'abord concevoir la structure, installer les réseaux et seulement ensuite — vous occuper de la finition. Dans le monde de la programmation, la deuxième approche s'appelle l'architecture hexagonale (également Ports & Adapters). Et Kotlin, avec sa concision et son puissant système de types, est le langage idéal pour concrétiser ce concept.
\n\nCet article n'est pas un manuel ennuyeux. C'est un guide pratique qui montrera comment l'architecture hexagonale en Kotlin transforme votre code en un système flexible, testable et indépendant des services externes. Nous analyserons un exemple à partir de zéro, écrirons du code réel et verrons comment cela fonctionne en pratique.
\n\nQu'est-ce que l'architecture hexagonale et pourquoi en avez-vous besoin ?
\n\nAlistair Cockburn, qui a inventé ce terme, comparait l'application à un hexagone. Au centre se trouve votre logique métier. Sur les bords se trouvent les ports (interfaces) par lesquels elle communique avec le monde extérieur. Les adaptateurs sont les implémentations de ces interfaces pour des technologies spécifiques : base de données, framework web, file de messages.
\n\nIdée clé : la logique métier ne sait rien de la façon dont les données sont stockées ou du framework utilisé. Elle appelle simplement les méthodes des ports. Vous voulez remplacer PostgreSQL par MongoDB ? Écrivez simplement un nouvel adaptateur. La logique métier ne remarquera même pas la substitution.
\n\nEn Kotlin, cela est particulièrement pratique grâce aux interfaces avec implémentations par défaut, aux classes sealed et au style fonctionnel. Passons aux choses sérieuses.
\n\nConception du système : exemple d'une boutique en ligne
\n\nSupposons que nous écrivions un microservice pour la gestion des commandes. Au centre se trouve OrderService, qui crée des commandes et vérifie la disponibilité des produits. Le monde extérieur est constitué d'un contrôleur HTTP et d'une base de données.
Voici à quoi ressemble la structure du projet :
\n\nsrc/\n├── domain/ # Cœur (logique métier)\n│ ├── model/\n│ ├── port/\n│ └── service/\n├── adapter/\n│ ├── input/ # Adaptateurs entrants (contrôleurs)\n│ └── output/ # Adaptateurs sortants (référentiels)\n└── Application.kt # Point d'entrée\n\n\nÉtape 1. Cœur : modèles et ports
\n\nCommençons par le modèle de commande. En Kotlin, ce sera une data class :
// domain/model/Order.kt\ndata class Order(\n val id: String,\n val customerId: String,\n val items: List<OrderItem>,\n val status: OrderStatus\n)\n\nenum class OrderStatus {\n PENDING, CONFIRMED, SHIPPED, CANCELLED\n}\n\ndata class OrderItem(\n val productId: String,\n val quantity: Int\n)\n\n\nDéfinissons maintenant les ports — les interfaces par lesquelles le cœur communique avec le monde extérieur. Nous aurons deux ports : un pour obtenir les données sur les produits, l'autre pour sauvegarder les commandes.
\n\n// domain/port/ProductRepository.kt\ninterface ProductRepository {\n fun checkAvailability(productId: String, quantity: Int): Boolean\n}\n\n// domain/port/OrderRepository.kt\ninterface OrderRepository {\n fun save(order: Order): Order\n fun findById(id: String): Order?\n}\n\n\nRemarquez : il n'y a nulle part de mention de base de données, HTTP ou frameworks. Une logique métier pure.
\n\nÉtape 2. Service métier
\n\nÉcrivons maintenant OrderService, qui utilise ces ports. En Kotlin, nous passons les dépendances via le constructeur — cela s'appelle l'injection de dépendances (DI).
// domain/service/OrderService.kt\nclass OrderService(\n private val productRepository: ProductRepository,\n private val orderRepository: OrderRepository\n) {\n fun createOrder(customerId: String, items: List<OrderItem>): Order {\n // Vérifie la disponibilité de chaque produit\n for (item in items) {\n if (!productRepository.checkA