Microservices : Domain-Driven Design et architecture

Thomas Ruiz
neoxia
Published in
13 min readMar 17, 2020

Dans notre article précédent, nous abordions le sujet de la CI/CD sous le prisme d’une architecture orientée microservices, et nous recommandions en conclusion de découper vos équipes métiers et techniques en fonction de domaines fonctionnels, afin qu’elles puissent travailler indépendamment et sans friction. Ce principe de regroupement des métiers et des technologies est issu d’une méthode de conception logicielle nommée Domain-Driven Design.

Nous allons, tout au long de cet article, vous donner une définition de ce qu’est le Domain-Driven Design, et vous expliquer comment cette méthode de conception peut intervenir dans la réalisation de vos projets logiciels orientés Microservices.

Les logiciels que nous utilisons au quotidien ne sont que des outils à notre service. Ils sont conçus pour être une abstraction, sous forme de code, d’un problème pratique et réel. La matérialisation de cette abstraction est faite par des professionnels du code, qui ont une compréhension souvent éloignée du métier pour lequel ils conçoivent ce logiciel, ce qui peut amener à des situations où le logiciel attendu par le client est très éloigné de la réalité produite. Ainsi, le Domain-Driven Design est une méthode de conception logicielle qui préconise de placer le domaine métier, et la compréhension de celui-ci, au centre du développement d’une application. Les bases de cette méthode ont été théorisées dès 2003 par Chris Evans à travers son livre Domain-Driven Design — Tackling Complexity in the Heart of Software, dans lequel il définit les bases de cette méthode de conception, à savoir :

  • les conceptions complexes doivent être basées sur un modèle ;
  • l’accent doit être mis sur le domaine et la logique associée.

À travers cet article, nous allons d’abord définir les concepts clés du Domain-Driven Design ; puis nous verrons comment les coupler aux microservices pour qu’à la fin de cet article, vous puissiez les développer avec une architecture qui a fait ses preuves et qui se révèlera un atout non négligeable lors de votre projet.

Concepts

La conception logicielle pilotée par le domaine s’articule autour de la compréhension des problèmes rencontrés par le métier, dans le but de créer des logiciels robustes, résilients et évolutifs.

Afin de faciliter cette compréhension, la coopération et la mise en place d’un langage commun est nécessaire. Autrement dit, un véritable dialogue doit être établi entre la personne qui connaît le mieux les problématiques du métier au quotidien, et la personne qui codera cette abstraction. Ce dialogue doit être formalisé par un langage structuré autour du domaine, et utilisé par tous les membres de l’équipe pour connecter les activités de l’équipe avec le logiciel. Nous allons voir dans la partie ci-dessous quels sont les termes et les concepts à utiliser dans le cadre du DDD pour faciliter la coopération.

Le domaine

Le domaine est le périmètre d’activité d’un métier bien défini pour lequel le logiciel est développé. Il est toujours orienté utilisateur, et a pour but de représenter les objectifs d’un système pour ses utilisateurs. Une bonne définition du domaine est primordiale pour s’assurer de la bonne compréhension des tenants et aboutissants du projet.

Par exemple, le domaine d’activité de la santé est éloigné du domaine de la vente en ligne, et les enjeux sont différents. Un logiciel qui gère l’inventaire d’un hôpital ne répondra pas aux même besoins qu’un logiciel qui gère l’inventaire d’un entrepôt, même si on ne s’en douterait pas au premier abord. Dans le DDD, on essaye de mettre le domaine au centre de l’application. Les personnes en charge du développement du logiciel doivent de ce fait s’y intéresser, et chercher à acquérir la meilleure compréhension possible du sujet, pour que les concepts métiers se retrouvent dans le code et puissent être facilement compris par tous.

Le modèle

Le modèle, quant à lui, est une abstraction du domaine. Il reprend différents aspects du domaine pour résoudre certains problèmes liés à ce métier. Cette abstraction est généralement faite par des experts du domaine. D’après notre expérience, il est préférable que l’équipe de développement soit impliquée dans la co-construction du modèle avec les experts du domaine. Cela permet à tous de bien le comprendre et d’avoir une idée de l’application dans son ensemble.

C’est le modèle qu’on va le plus utiliser quand on parle de DDD. Dedans, on y met tous les éléments relatifs au domaine : ses contextes, ses entités, leurs relations et attributs,… On le représente souvent avec un schéma.

Exemple de modèle tiré du domaine d’assurance santé

Le langage partagé

Il s’agit de l’ensemble des mots/phrases construites autour du modèle, et utilisées par tous les membres des équipes pour relier les différentes activités du domaine avec le logiciel. Ce langage doit être défini par les différents métiers qui ont un rôle à jouer dans le projet, ainsi que par l’équipe technique, pour que ces termes soient retrouvés dans la couche métier du code. Ce langage partagé est primordial pour la bonne réussite du projet. Avec des termes compris par tout le monde, la communication devient plus facile, et toutes les parties prenantes se comprennent mieux. Ainsi, le Product Owner peut rédiger des user stories qui sont comprises universellement, et ont un sens pour le métier. Les résultats des tests End-To-End (E2E, qui permettent de tester une application dans son ensemble pour vérifier notamment la communication entre les sous-ensembles du système) et d’intégration peuvent également être partagés et compris par des personnes non techniques. Enfin, le code lui même représente les différents objets et actions du métier, ce qui offre une maintenabilité bien supérieure, avec un design d’architecture bien plus compréhensible.

Le contexte borné

Lorsqu’on modélise un domaine, il est souvent difficile de construire un modèle qui, avec un seul langage partagé, parle à tous les métiers d’une entreprise. Par exemple, si on essaie de modéliser le domaine d’une entreprise de vente en ligne, tous les métiers savent ce qu’est un produit, mais ce qui relève de la gestion des stocks de ces produits ne parle uniquement qu’aux métiers de l’entrepôt. De même, les transactions financières ne concernent pas l’équipe de vente, qui n’est concernée que par son chiffre d’affaires. Il faut pourtant toujours que les différents métiers de l’entreprise puissent communiquer. C’est là que le contexte borné intervient : il divise le domaine global en sous-domaines qui peuvent être compris par l’équipe concernée. Les termes qui apparaissent dans plusieurs contextes sont les liens qui définissent le modèle.

Deux contextes bornés : Inventaire et Commande

L’entité

Dans le modèle, les objets du domaine sont représentés par des entités. En clair, une entité est définie par ses attributs et ses relations aux autres entités. Par exemple, dans le domaine de l’assurance santé, un assuré est une entité, tout comme un plan d’assurance, une feuille de soins, une mutuelle… On regroupe ces entités dans des contextes bornés pour simplifier le modèle, et on se sert d’entités “transverses” comme ponts entre ces contextes. Par exemple, toujours dans le domaine de l’assurance santé, on pourrait imaginer les contextes “remboursement” et “vente”. Différentes entités propres à ces contextes peuvent ainsi être séparées, mais on a toujours une notion d’assuré (remboursement à un assuré et nouvel abonné). On peut donc voir que le pont entre ces contextes est l’entité “assuré”. Ses attributs peuvent changer en définition du contexte. Pour le remboursement, on peut parler de RIB, de montant remboursé au total,… Pour la vente, on parle plutôt d’adresse mail, de type de prospect, de plan choisi,…

En résumé

Ainsi, quand on parle de DDD, on parle, entre autres, de ces cinq concepts généraux, qui sont tous intimement liés. Le domaine est représenté par un modèle, lui-même découpé en contextes bornés et en entités, qui sont tous définis par un langage commun.

Chez Neoxia, nous adoptons souvent ces principes dans des projets conséquents. Beaucoup peut être dit sur le DDD (et à dire vrai, un livre de presque 600 pages a été rédigé dessus). Nous n’avons pas parlé d’agrégat, de CQRS, d’Event Sourcing, de Value Object… Ce sont des principes DDD appliqués au développement, et qui constituent le coeur d’applications complexes.

Relation avec les microservices

Pour en revenir à notre sujet de départ, les différents concepts évoqués plus haut vont trouver leur place dans les trois couches qui composent un microservice.

L’architecture hexagonale

Un microservice (et toute application s’appuyant sur les principes DDD) est composé d’au moins trois couches d’abstraction : la couche applicative, la couche métier et la couche infrastructure. On appelle cela une architecture en anneaux (Ring Architecture), connue aussi sous le nom d’architecture hexagonale (Hexagonal Architecture). Pour creuser le sujet, visitez ce lien.

La couche d’application

Cette couche contient la partie opérationnelle du microservice qui permet d’exposer les objets métiers vers l’extérieur. C’est elle qui va pouvoir implémenter les différentes interfaces de la couche métier et faire le lien entre les différentes parties du code. C’est le point d’entrée, mais également le point de sortie de l’application, d’où son nom. C’est ici qu’on charge la configuration, qu’on instancie toutes les classes (pour permettre l’injection de dépendances), et qu’on implémente les fonctionnalités métier.

Voici un exemple de classe en PseudoCode. Nous suivrons le même fil directeur d’un microservice qui peut réserver des salles de réunion.

Comme vous pouvez le voir, nous avons ici un mélange d’anglais et de français. On essaiera de garder les noms techniques en anglais, mais de s’en tenir au langage partagé (en français, dans notre contexte) pour tous les termes métiers (ici Salle, Horaire, Réserver et Réservations). La couche applicative se charge donc de charger la configuration de l’application et d’instancier les différentes classes de la couche infrastructure (ici ReservationsRepository et SallesRepository) et de la couche applicative (Service). Elle déclare ensuite une nouvelle route (POST /reservations) qui récupère les paramètres de la requête, trouve la salle associée et la réserve, puis gère les cas spéciaux.

Elle implémente également l’interface Domain.ReservationsService qui pourra gérer les trois cas métiers nécessaires au bon fonctionnement du microservice : vérifier si une salle existe, vérifier qu’elle n’est pas déjà réservée pour cet horaire, et réserver la salle. Concernant les tests, il faut surtout tester les différents services implémentés. On peut également tester les contrôleurs, ou points d’entrée de l’API, toujours unitairement. Pour tous ces tests unitaires, il est nécessaire de mocker les différentes dépendances. Puisqu’il ne devrait jamais y avoir de dépendance externe au système dans cette couche, cela devrait être chose aisée. Il ne faut pas hésiter à utiliser des stubs plutôt que des entités complexes (mais dans notre exemple, l’utilisation de Reservation et Salle est tout à fait possible). Enfin, c’est toujours dans cette couche que l’on trouvera les tests fonctionnels, ou end-to-end, du microservice.

C’est un cas très simplifié, mais on voit ici que la couche applicative contient en fait la plus grosse partie du code. On voit donc que c’est bien elle qui fait le lien entre les différentes couches. C’est ici qu’on pourrait également trouver la récupération de l’utilisateur identifié, la vérification de ses droits, un rate limiter, un circuit breaker, un système de tracing, des logs, etc. Bref, vous l’aurez compris, toutes ces choses un peu techniques qui n’ont rien à faire dans la couche métier, que nous allons voir tout de suite.

La couche métier

Cette couche représente les concepts propres au domaine métier que le microservice peut traiter dans le système d’information. C’est là où l’on pourra trouver les différentes entités du système, leurs attributs et les différentes interfaces à implémenter au sein de la couche applicative. La notion de Domain Model étant très importante, il est indispensable de bien découper ses contextes afin d’avoir une cohérence dans les entités. On pourra également trouver des classes qui n’ont pas de dépendances à autre chose que du métier. Dans notre exemple, cela pourrait être une classe qui calculerait des statistiques de réservation d’une salle sur une période donnée, par exemple.

Dans notre exemple, le domaine métier auquel répond montre microservice est vraiment basique et ne nécessite même pas d’explication. En vérité, dans la plupart des microservices, les explications ne seront pas nécessaires : et c’est là tout l’avantage par rapport à un monolithe ! Le domaine métier par microservice devrait être suffisamment restreint pour être compréhensible par lui-même. On voit ici qu’une seule chose est possible : réserver une salle. Trois choses peuvent arriver : soit la salle n’existe pas, soit la salle n’est pas disponible (pour cet horaire), soit tout va bien, la salle est réservée et on émet un événement de type NouvelleReservation.

C’est une bonne pratique de découpler le service des tâches techniques qui peuvent être implémentées dans une autre couche, voire un autre microservice. Typiquement, on pourrait très bien imaginer un microservice qui gère les cas spéciaux comme deux réservations au même moment, un envoi de mail, un appel à un service externe pour mettre à jour un calendrier… Toutes ces fonctionnalités peuvent être gérées grâce aux évènements, qui permettent d’avoir un retour quasi-instantané pour l’utilisateur, et permet d’exécuter d’autres tâches de façon asynchrone.

Au sujet des tests, il n’est pas question de faire autre chose que des tests unitaires sur cette couche, et ils ne concernent que les entités, uniquement quand c’est nécessaire (pas besoin de tester des getters simples). Typiquement, on pourrait par exemple tester qu’un ensemble de données peut calculer différentes statistiques, ou bien qu’une monnaie peut être convertie dans une autre grâce à un taux. Il y a également un autre type de tests moins connu, qu’on appelle des tests de contrat. Ces tests vérifient qu’une implémentation d’une interface respecte bien, comme son nom l’indique, le contrat qu’elle spécifie. Ils sont toutefois plus difficiles à implémenter correctement.

La couche infrastructure

Enfin, la couche infrastructure va être “l’anneau” qui fait le lien entre l’application et le “monde extérieur”. C’est elle qui sera responsable de communiquer avec d’autres services, une base de données, un système de cache… mais également qui devra lancer un serveur HTTP pour écouter les connexions entrantes et rediriger vers la couche applicative. On y met donc beaucoup de choses !

Il s’agit vraiment ici de code technique, bien que peu complexe. Pour ce qui est des tests, cette fois-ci nous n’y échapperons pas : il faut faire des tests d’intégration. C’est le moment de démarrer une base de données, des mocks d’APIs et des faux serveurs SMTP, et de tester vos intégrations avec des systèmes externes. Ne perdez pas de temps à tester unitairement votre couche infrastructure, vous perdriez beaucoup trop de temps à mocker des classes qui ne sont pas les vôtres, et vos tests n’en seraient que moins solides.

Le DDD : quel intérêt ?

Les principaux intérêts qu’on peut tirer du DDD sont en fait des garanties. D’abord, tant que votre code est testé — ce que cette architecture vous permet de faire plus simplement — vous serez sûrs que vos microservices seront solides et fonctionneront selon vos attentes. La maintenabilité de l’application sera également bien supérieure. Avec une séparation des responsabilités bien claire, vous retrouverez facilement dans quelle partie de code un bug pourrait survenir, et vous n’aurez pas à faire le choix difficile de l’endroit où ajouter de nouvelles fonctionnalités.

Ainsi, vous aurez la garantie d’un microservice maintenable, testable, robuste et avec une faible adhérence aux autres composants ; quatres promesses que les applications monolithiques ne peuvent pas faire. Mais les avantages ne s’arrêtent pas là ! D’un seul coup d’œil, vous pourrez voir quel rôle remplit le microservice dans le système. Vous aurez une vision métier d’ensemble claire. Vous pourrez également voir rapidement où se situent vos potentiels problèmes de consistance. En effet, au sein d’un microservice, il paraît peu probable que des données deviennent inconsistentes (on y reviendra, mais pensez à utiliser des transactions !). Par contre, entre les microservices, tout peut arriver : une erreur non gérée quelque part, un problème de doublons, etc.

Le DDD peut vous aider à garder tout cela sous contrôle, en posant les bons termes sur tout ce que contient votre application. Les entités regroupées en un bloc consistant nommé Aggregate Root sont en fait regroupées dans un seul microservice. Si votre microservice devient trop gros (c’est-à-dire difficile à maintenir, responsable de trop de choses…), séparez le en plusieurs microservices, et créez-en un autre qui se chargera de maintenir la consistance de vos données, et qui sera le point d’entrée pour d’autres microservices.

Conclusion

La vision des objets métiers et de leurs périmètres existe déjà au sein des entreprises, il faut que celle-ci soit partagée avec les développeurs, et à partir de ce dialogue, mettre en place votre langage partagé. Une fois que ce langage devient un référentiel partagé, il sera plus facile de découper vos microservices et de créer un service par objet métier.

Casser les silos existants entre la technologie et les métiers est d’une importance capitale pour l’évolutivité du système d’information d’une entreprise. Un parc logiciel ne doit pas être figé dans le temps, mais évoluer avec les nouveaux usages des collaborateurs et des clients. Grâce à la conception pilotée par le domaine, le développement des applications répond directement aux enjeux métiers du moment en garantissant des modifications et évolutions simples dans le futur. Pour réussir cette transformation vers un système d’information modulaire, il faut que les travaux soient suivis par toute l’entreprise, que tous les acteurs soient embarqués. et qu’une vision politique soit impulsée et partagée. Nous consacrerons donc notre prochain article à l’organisation de l’entreprise et de ces acteurs autour de la conception logicielle pilotée par le domaine

--

--