Préambule
Cet article est le quatrième d’une liste de 5 chapitres, concernant l’architecture logicielle, afin de vous aider à analyser vos besoins, et à choisir l’architecture la plus adaptée pour réaliser votre projet.
- L’architecture Monolithique
- L’Architecture Orientée Service ou « Service Oriented Architecture (SOA) »
- L’Architecture Orientée Événement ou « Event Oriented Architecture (EOA) »
- Architecture Moderne : Les concepts essentiels
- Architecture Moderne : Exemple de création d’un nouveau projet
Lire les articles précédents est recommandé pour une meilleure compréhension du présent article.
Principe d’une architecture
L’architecture d’une application, ou d’un système d’information (SI) définit les différents intervenants (module de gestion des utilisateurs, module de tarification, …) qui la composent, et les interactions (protocoles, relations, …) entre chacun d’entre eux.
Dans une architecture moderne, on appelle “services” les intervenants/modules/processus qui composent l’application. On peut les différencier grossièrement (Appli Web, Base de données, …), ou avec précision (Service d’envoi des e-mails, Frontend Web, Backend, Appli Mobile, …).
Les “interactions” peuvent être nombreuses, et de nature très différentes. En effet, des services communiquent en se basant sur un protocole (Socket, HTTP, AMQP, …) de plus ou moins haut niveau.
Le but d’une architecture est de fournir une structure adaptée aux besoins, de manière à simplifier la maintenance, et de permettre une forte évolutivité aux applications développées.
La mise en place d’une architecture se fait généralement (mais pas nécessairement) en début de projet, ou lors de la refonte d’un projet existant. Celle-ci nécessite absolument une analyse, la plus poussée possible, prenant en compte le plus possible d’éléments métiers, et en anticipant aux maximum les futures évolutions/besoins et problèmes.
Il existe plusieurs “niveaux” d’architecture. En effet, au sein d’une architecture orientée service par exemple, un service pourrait tout à fait être développé en se basant sur un patron d’architecture tel que le MVC. Dans ce cas, le service fait partie d’une architecture plus vaste.
Types d’architectures
Nous verrons ici trois types d’architecture importantes : Monolithique, Orientée Service, et Orientée Événement.
Je vous invite à lire les articles concernant ces trois architectures, étant donné que le présent article n’en fait qu’un résumé.
Architecture Monolithique
Simple à comprendre, car elle n’est composée que d’un seul bloc : une application qui fait absolument tout ! Il s’agit de la plus vieille architecture, on ne l’inclut donc pas dans les architectures modernes.
Avantages
- Simple à développer
- Simple à déployer
- Peu coûteuse sur le (très) court terme
- Ne nécessite pas de connaissance spécifique pour être utilisée
Inconvénients
- Maintenance du code de plus en plus compliquée/contraignante
- Nouvelles fonctionnalités de plus en plus longues à développer
- Application difficile à optimiser
- Application lourde et longue à compiler/démarrer
- Très dépendante des technologies employées initialement
- Difficulté à intégrer de nouvelles ressources humaines
- Difficulté à faire collaborer plusieurs équipes
Architecture Orientée Service
Aussi appelée “Architecture Microservice”, elle se compose d’un nombre illimité de services. Ces services forment un ensemble de blocs, capables de communiquer entre eux grâce à des APIs. Elle repose donc sur un système de traitement synchrone principalement. Sa structure n’est pas forcément linéaire, mais permet l’interaction entre n’importe quel service. Nous avons ainsi la possibilité de créer une infinité de services, sans complexifier les relations entre les services existants.
Les principaux avantages de cette architecture sont les suivants :
- Permettre une évolution constante (ajout de nouvelles fonctionnalités, refonte/mise à jour des modules obsolètes)
- Grande scalabilité (haute disponibilité)
- Plusieurs équipes peuvent travailler sur l’application en parallèle (répartition des services)
- Meilleure maintenance
- Possibilité de développer chaque services dans des technologies/versions différentes
Mais elle possède également des inconvénients :
- Plus complexe à mettre en place qu’une architecture monolithique (nécessite des connaissances plus poussées)
- Plus coûteuse sur le court terme (temps d’analyse et de mise en service plus conséquent)
Architecture Orientée Événements
Cette architecture se compose également d’un nombre illimité de services. Mais la grande différence avec l’architecture orientée service, c’est qu’elle ajoute la notion d’événement. Un événement est un message, stockée temporairement, afin d’être extrait à un moment indéterminé par un autre service. Elle repose donc sur un système de traitement asynchrone.
Là où l’architecture orientée service utilise un système d’API pour communiquer entre différents services, l’architecture orientée événements utilise un Message Broker, permettant d’acheminer les messages d’un service à un/plusieurs autres services.
Les principaux avantages de cette architecture sont les suivants :
- Permettre une évolution constante sans impacter les performances des services existants
- Grande scalabilité (haute disponibilité)
- Plusieurs équipes peuvent travailler sur l’application en parallèle (répartition des services)
- Meilleure maintenance
- Possibilité de développer chaque services dans des technologies/versions différentes
Mais elle possède également des inconvénients :
- Plus complexe à mettre en place qu’une architecture monolithique, et même orientée service (nécessite des connaissances plus poussées)
- Plus coûteuse sur le court terme (temps d’analyse et de mise en service plus conséquent)
Traitements Synchrones et Asynchrones
Les architectures modernes tendent à nous faire créer de nombreux services. Mais comment ces services communiquent-ils entre eux ? On différencie deux types de communication : Synchrone et Asynchrone.
Synchrone
Le traitement Synchrone est probablement le plus simple à mettre en place, mais il pose quelques problèmes.
Avantages
- Simple
- Temps Réel
Inconvénients
- Bloquant
En effet, dans un traitement synchrone, un client qui établit une communication avec un serveur, est obligé d’attendre la fin du traitement pour avoir une réponse. Par exemple : Si le navigateur exécute une requête HTTP vers un serveur, et que celui-ci est chargé d’envoyer quelques milliers d’e-mails. En synchrone, le navigateur devra attendre que tous les e-mails soient envoyés, avant de pouvoir rediriger l’utilisateur en lui disant “les e-mails ont été envoyés”.
Asynchrone
Le traitement Asynchrone est plus complexe, mais il permet de résoudre le problème énoncé ci-dessus.
Avantages
- Non Bloquant
Inconvénients
- Complexe
- Différé
En effet, dans ce cas, le client n’a pas besoin d’attendre la fin du traitement pour pouvoir continuer à travailler. En reprenant l’exemple précédent, en asynchrone cette fois, le navigateur exécute une requête HTTP vers un serveur. Celui-ci n’effectue pas l’envoi des e-mails directement, mais “enregistre la demande”. Ainsi, le traitement ne prend que peu de temps, et il peut retourner une réponse HTTP 200 au navigateur, lequel pourra afficher comme message à l’utilisateur “les e-mails vont être envoyés”. A un moment donné, un processus autonome sur le serveur traitera la demande, et enverra les e-mails. Suite à cela, une notification pourrait être envoyée au client (WebSocket, e-mail, message en base, etc…) pour l’informer que les e-mails ont bien tous été envoyés.
Conclusion
Ces deux types de traitements répondent à des problématiques différentes. Le traitement asynchrone doit être privilégié lorsque cela est possible, mais les deux types peuvent tout à fait cohabiter, pour répondre à des problématiques différentes.
Les bases de données
Comment séparer les données
Combien de bases de données faut-il déployer ? Combien de serveurs de base de données ? Quels types de base de données ?
Il existe 2 possibilités.
Shared Database
Cette solution consiste en une seule base de données, partagée entre les service. Elle est clairement à éviter et possède de nombreux défauts tels que :
- Risques de conflits (plusieurs applications qui utilisent le même nom de table)
- Moins sécurisé (même si l’on peut restreindre les accès d’un utilisateur à certaines tables, les bibliothèques (ORM) comme Doctrine ont généralement besoin de pouvoir créer de nouvelles tables
- Les performances d’un service dépendent des autres services également (ex: table verrouillée par un autre service)
Bien qu’elle possède quelques avantages :
- Simplicité pour faire des transactions touchant les données de différents services
- Plus simple à déployer
- Peu coûteux
Database Per Service
Cette solution consiste à séparer les données de chaque service dans des base de données distinctes (voir sur des serveurs de base de données distincts).
Nettement plus élégante que la première solution, elle pose malgré tout quelques difficultés (bien qu’il existe des solutions que nous verrons plus tard) :
- Complexité liée à l’implémentation d’une transaction intervenant sur plusieurs bases de données (exemple de tables : Utilisateur + Paiements, lesquelles appartiendraient à deux service distincts : Utilisateur + Facturation), ou de faire des jointures touchant à plusieurs domaines.
- Plus coûteux en termes de ressources dans le cas où l’on sépare en plusieurs serveurs de base de données.
Mais les avantages de cette solution sont très clairs :
- Services totalement indépendants, et seuls maîtres de la gestion de leurs données (dans le cas d’un serveur par service). Un service peut stocker ses données sous n’importe quelle forme (MySQL, MongoDB, …), et donc choisir la solution la plus adaptée (même la version du serveur).
- Peut être facilement sécurisé (un utilisateur par base de données).
Les Jointures en Database Per Service
Il s’agit d’un cas que l’on rencontre forcément avec la méthode précédente. Prenons donc un exemple concret avec une application constituée d’un service “User”, et d’un service “Billing”.
En Database Per Service, nous avons deux bases de données différentes. Imaginons que ces deux bases de données soient sur des serveurs MySQL distincts. Nous avons donc une base de données “user” et une base de données “billing”. On prend MySQL comme exemple, mais on pourrait avoir du PostgreSQL pour l’un, et du MongoDB pour l’autre, donc le concept de “jointure” est large.
La première contient des tables comme “user”, “group”, “right”, etc…
La seconde contient des tables comme “bill”, “payment”, etc…
Une facture (table “bill”) est rattachée à un utilisateur (table “user”). Mais ces deux tables sont sur des bases (voir même des serveurs) distincts. Impossible donc de définir une clé étrangère, et de faire des jointures dans nos requêtes. Néanmoins, la table “bill” possède un champ “user_id”.
Imaginons maintenant que l’on veuille créer un service “Bill History”, permettant de consulter toutes les factures émises. Ce service aura besoin de récupérer toutes les lignes présentes dans la table “bill”, mais également les lignes de la table “user” pour avoir le nom et le prénom de l’utilisateur correspondant à chaque facture.
Si les deux tables étaient dans une même base de données, nous aurions pu faire la requête suivante :
SELECT bill.amount,
bill.created_at,
user.first_name,
user.last_name
FROM bill
INNER JOIN user ON user.id = bill.user_id;
Malheureusement, avec des bases de données isolées (physiquement, avec des droits adaptés, etc…), cette requête n’est pas possible.
Il existe bien sûr des solutions pour palier à ce problème :
L’Architecture Orientée Service utilise “l’API Composition”, c’est-à-dire qu’un service A, qui aurait besoin de données que possède un service B, ferait alors une requête vers ce dernier pour obtenir les données dont il a besoin pour traiter la requête.
L’Architecture Orienté Événement utilise les événements afin de constituer sa propre base, laquelle contient une copie des données réelles. Ainsi, lorsqu’un événement est créé (ex: « user_created »), le service enregistrerait dans sa propre base de données une copie des informations qui l’intéressent (ex: Id + Nom + Prénom de l’utilisateur). Cette dernière solution présente également un avantage non négligeable en termes de performances, puisque chaque service peut alors stocker les données uniquement sous la forme qui lui est la plus bénéfique (données calculées, table réunissant les données de plusieurs tables, etc…). Ce qui est à la base une mauvaise pratique en base de données devient alors une solution élégante et performante.
Les Transactions en Database Per Service
Nous avons vu précédemment comment résoudre le problème des jointures, mais un autre problème persiste : les transactions.
Pour rappel, une transaction a pour but d’effectuer un ensemble d’actions liées de manière à les rendre indissociables : tout ou rien. C’est-à-dire que si une requête échoue, il faut annuler tout le traitement (les requêtes exécutées précédemment) afin de pas avoir de données incohérentes en base.
MySQL (et de nombreux SGBD intègrent ce principe de Transaction), mais lorsqu’on fait intervenir plusieurs services différents, chacun possédant sa propre base, comment maintenir ce principe ?
SAGA
Si vous avez déjà intégré un système de paiement à votre application, vous êtes alors probablement familier avec ce concept.
Imaginez la situation suivante : Un site de e-commerce, sur lequel l’article pourra être payé via PayPal. Nous faisons intervenir un tier dans cet exemple, mais PayPal pourrait tout à fait être un service interne à notre application, cela ne changerait rien.
Ainsi, lorsque l’utilisateur clique sur le bouton “payer”, nous devons faire plusieurs choses :
- Réserver un article en stock
- Enregistrer la commande avec l’état “en attente”
- Rediriger l’utilisateur sur PayPal pour qu’il valide le paiement
PayPal répond “le paiement a bien été effectué” :
- Passer la commande à l’état “payée”
- Étapes permettant d’acheminer le produit jusqu’à l’utilisateur
Mais si le paiement est refusé (que l’utilisateur ne va pas jusqu’au bout, ou qu’il ne dispose pas d’un solde suffisant), dans ce cas, le workflow suivant se produira :
- Réserver un article en stock
- Enregistrer la commande avec l’état “en attente”
- Rediriger l’utilisateur Sur PayPal pour qu’il valide le paiement
Aucune réponse pour le paiement dans le délais imparti
- Annuler la réservation de l’article en stock
- Supprimer la commande
Dans les deux cas, nous avons une transaction en minimum deux étapes, étant donné que nous dépendons de la réponse de PayPal. Nous sommes obligés d’enregistrer les informations en base en attendant d’avoir la validation de PayPal. Mais dans le cas où le paiement est infructueux, il faut annuler tout ce qui a été fait auparavant.
En architecture orientée service ou événement, lorsque le processus débute, on crée ce qu’on appelle une Saga. Cette Saga raconte l’épopée de la commande, ou bien le Workflow, depuis sa création, jusqu’à sa validation ou suppression. On gère ainsi un/des objets possédant un “statut”. C’est ce statut qui déterminera l’état de la commande, qui évoluera dans un sens ou dans un autre, et qui effectuera les actions associées au changement de statut correspondant (que ce soit dans le but d’avancer, ou bien d’annuler tout ce qui a été fait).
En Architecture Orientée Service, chaque service, lorsqu’il a terminé son traitement, appellera l’API d’un ou de plusieurs autres services. Par exemple, le service en charge d’enregistrer le paiement, une fois le paiement validé, appellera l’API du service en charge des commandes, afin de le notifier que le paiement a bien été effectué. Ce dernier service pourra ainsi mettre à jour le statut de la commande.
En Architecture Orientée Événement, c’est la même chose, mais au lieu de faire appel à l’API d’un autre service, on créera tout simplement un événement. Les services intéressés par cet événement pourront ensuite lire le message afin de faire le traitement adéquate (ex: mettre à jour le statut d’une commande).
Cette solution consiste donc à faire abstraction du système de gestion de base de données, afin d’implémenter une transaction “fonctionnelle” devant aboutir d’une manière ou d’une autre. Il s’agit ici de prendre en compte toutes les issues potentielles, et non la solution où “tout fonctionne correctement du premier coup”.
Pour simplifier la compréhension et l’implémentation d’un tel système, l’idéal est de visualiser chaque service comme étant géré par une société tierce. Il faut ainsi coordonner les tâches, et s’assurer de la cohérence des informations en prenant en compte le différé de communication (le temps qu’une information provenant d’un service soit traitée par les autres services).
Services multi-thread
A première vue, on pourrait penser qu’un service doit correspondre à un seul et unique processus. Dans certains cas, c’est possible, mais pas toujours.
En effet, nous avons vu deux types d’architectures modernes, et donc deux types de traitements, synchrones et asynchrones :
- Dans le cas d’un traitement asynchrone, on utilisera un protocole adapté tel que AMQP.
- Dans le cas d’un traitement synchrone, HTTP est tout à fait adapté.
Mais dans certains cas, un service doit être capable de faire les deux.
Exemple
Imaginez un micro-service “Statistics”. Ce service aurait à charge de collecter des données (visites d’un site web) afin d’établir des statistiques (ex: nombre de pages visitées par un même utilisateur, nombre de visites par page, etc…).
Ce service devra ainsi répondre à deux problématiques :
- Collecte de données
- Consultation des statistiques
Pour collecter les données, il vaut mieux mettre en place un traitement asynchrone, avec un protocole tel que AMQP. Le service permettant de consulter les pages du site web n’aurait qu’à créer un message contenant les informations de la visite. De cette manière, notre service “Statistics” pourra définir un “Consumer” qui s’occupera de lire chacun de ces messages, afin de mettre à jour sa base de données.
Mais si ce service ne permet pas de consulter les statistiques, il n’a alors aucune utilité. Il y aurait alors 2 solutions :
- Créer un autre service “Statistics API” qui aurait accès à la même base de données, mais cela ne respecterait pas le principe de “Database Per Service”.
- Créer un seul service, lequel exécutera plusieurs processus (threads) en parallèle. Le premier thread sera le “Consumer”, alors que le second sera un serveur HTTP par exemple.
La première solution est tout à fait possible, mais correspond à l’anti-pattern “Nano Service” (que l’on verra plus tard). La deuxième solution, elle, permet de conserver un code unique, répondant entièrement à la problématique, et correspondant à la responsabilité “Statistiques”.
Suivant les technologies utilisées, il existe différentes manières de gérer cette situation. A Odeven par exemple, nous utilisons essentiellement Symfony pour nos services de type « back », et RabbitMQ (AMQP) pour gérer les événements. Ainsi, un service qui doit consommer des messages pour les stocker en base, et qui doit également mettre à disposition une API HTTP, repose sur Docker, avec supervisor, afin d’exécuter 2 processus en parallèle lorsque le container démarre : le serveur HTTP + la commande Symfony permettant de consommer les messages AMQP (le composant messenger).
L’infrastructure Système
Si vous développez une application, l’utilisation qui en sera faite va impacter le choix de l’architecture. Dans le cas d’une application mobile qui ne fait appel à aucun serveur, il n’y a pas vraiment de question à se poser puisqu’elle sera installée sur les terminaux concernés.
Mais dans le cas d’une application Web, il peut y avoir des questions très pertinentes :
- Est-ce que l’application sera utilisable par tous sur internet ?
- Est-ce que l’application sera utilisable pour un/des clients uniquement ?
- Pour une utilisation intranet ?
- Pour une utilisation SaaS
Si vous développez un site tel que Facebook, Youtube, etc… vous n’avez pas réellement de client, vous avez des utilisateurs. C’est à vous de gérer l’hébergement de votre site/application Web.
Mais dans l’autre cas, c’est-à-dire si vous développez une application que vous distribuez à plusieurs clients par exemple, vous devrez vous poser la question suivante : L’application sera-t-elle installée sur le serveur du client ou sur notre serveur ?
En répondant à ces questions, vous saurez si l’application devra être hébergée par vous, ou vos clients. Mais dans le cas où vos clients sont multiples (pour une même application), vous devez vous interroger sur la séparation des données. Exemple :
- Une seule base de données avec un “CUSTOMER_ID” dans chaque table
- Une base de données par client
- Une instance de l’application par client
La première solution n’est généralement pas la meilleure, car elle vient complexifier la structure de votre base de données, et donc les requêtes.
La seconde est avantageuse, car elle permet de gérer de manière simple et fiable la sécurité des données (un client = un utilisateur ayant les droits sur une seule base de données).
La dernière permet une plus grande souplesse (version différente de l’application pour chaque client, séparation des fichiers, performances garanties par client, etc…), mais est plus coûteuse en terme de maintenance (plus difficile à mettre à jour et plus lourd). Dans le cas d’une application installée sur le serveur des clients, vous n’aurez malheureusement pas le choix.
Il existe en réalité une infinité de possibilités, mais ces exemples sont des bases que l’on voit fréquemment.
L’architecture dans tout ça
Si vous hébergez vous-même l’application, vous maîtrisez totalement l’environnement et vous pouvez faire les choix qui vous plaisent. Mais si l’application doit être installée chez vos clients, c’est une autre affaire. Et dans le cas d’une architecture orientée service ou orientée événements, cela pourrait paraître insurmontable.
Pourtant, la solution est en faite très simple. Aujourd’hui, il existe une solution bien connue du nom de Docker (il existe de nombreuses autres solutions de conteneurisation). Sans entrer dans les détails, cette solution permet de faire tourner des applications/services dans des containers (sortes de Machines Virtuelles). Ces containers peuvent tourner n’importe où (sur un serveur Linux/Windows/MacOS). Vous n’avez donc plus à vous préoccuper du serveur du client. Si vous souhaitez en savoir plus, je vous invite à lire ce tutoriel.
Avec Docker-Compose, vous pouvez aller encore plus loin, et créer des applications “Multi-Container”. Dans le cadre d’une architecture orientée service, vous pourriez donc faire le choix d’un service = un container.
Avec cette solution, le seul prérequis que vous aurez auprès de vos clients est qu’ils installent Docker/Docker-Compose. Ainsi, peu importe la complexité de votre architecture, elle pourra être déployée intégralement, et n’importe où, en créant juste les bons fichiers de configuration.
Aperçu général d’une architecture moderne
Un récapitulatif général des différents aspects à prendre en compte lorsqu’on souhaite utiliser une architecture moderne pourrait être le suivant :
- Infrastructure Système
- Où l’application doit-elle être déployée (sur nos serveurs, ceux du client) ?
- Quelles ressources sont disponibles ?
- Quel besoin en performance ?
- Comment déployer ?
- Communication entre les services
- Quel(s) protocole(s) pour la communication interne ?
- Quel(s) protocole(s) pour la communication externe ?
- Quelle vitesse ?
- Synchrone ? Asynchrone ?
- Stockage des données
- Plusieurs système de gestion de bases de données ?
- Plusieurs bases de données ?
- Services
- Quelle technologie(s) ?
- Quelles sont les relations avec les autres services ?
Comme vous pouvez le constater, il ne s’agit que d’architectures, c’est-à-dire qu’elles n’imposent absolument aucune technologie ! Son but est simplement de structurer votre application avec des principes.
Exemple
Dans cet exemple, nous allons utiliser à la fois l’Architecture Orientée Service et l’Architecture Orientée Événements.
Définition simple du domaine
L’application à concevoir est une application Web de e-commerce permettant aux utilisateurs de :
- Lister les articles
- Ajouter un article à son panier
- Valider son panier
- Payer sa commande
- Visualiser l’état de la commande
Architecture
Comme on peut le constater, nous implémentons également CQRS (cf. L’Architecture Orientée Événement). Nous avons ainsi deux catégories de services : Les Queries (OrderQuery), et les Commands (OrderCommand, PaymentCommand, et DeliveryCommand).
Voici la description de chaque service :
AMQP
Ce service fait tout simplement tourner une instance de RabbitMQ pour que les services puissent communiquer avec le protocole AMQP.
Web App
Contient un serveur HTTP (Apache), distribuant le code front (HTML, CSS, JS) de l’application. Cette application s’exécute ensuite sur l’ordinateur de l’utilisateur (via son navigateur).
API Gateway
Basé sur le design pattern du même nom, ce service contrôle les requêtes (ex: utilisateur bien connecté ?), et fait office de reverse proxy. Les appels aux queries sont directement “forward” aux services concernés, alors que les appels aux méthodes telles que “createOrder”, ou “createPayment” sont converties en AMQP (création d’un message “CreateOrderCommand”, “CreatePaymentCommand”, etc…). De cette manière, ces actions sont traitées de manière asynchrone.
OrderQuery
Au moins deux processus doivent tourner sur ce service : une API HTTP (donc un serveur web) permettant à l’API Gateway d’obtenir des informations sur les commandes, et un Worker.
L’API ne doit rien créer/modifier en base, et ne doit émettre aucun événement.
Le Worker, de type “Consumer”, tourne en arrière plan afin d’alimenter sa base de données. Afin de pouvoir fournir toutes les informations proposées par l’API, celui-ci doit consommer les événements suivants :
- OrderCreated
- OrderNotTreatable
- OrderReadyForDelivery
- OrderSent
- OrderDelivered
Comme vous pouvez le constater, ce service n’écoute que des événements. Ces événements lui permettent de connaître l’état de la commande des utilisateurs, et de stocker cet état en base.
OrderCommand
Ce service se résume à un Worker, qui consomme les commandes et événements suivants :
- CreateOrder
Celui-ci peut également émettre les événements suivant :
- OrderCreated
Son rôle est simple : lorsqu’une commande doit être créée, il va l’enregistrer en base, et émet l’événement “OrderCreated”.
PaymentCommand
Ce service se résume à un Worker, qui consomme les commandes suivantes :
- CreatePayment
Celui-ci peut également émettre les événements suivants :
- PaymentCreated
Son rôle est simple : lorsqu’un paiement doit être créé, il va contacter le service de PayPal pour vérifier l’authenticité du paiement. Si celui-ci est correct, il enregistre le paiement et émet l’événement “PaymentCreated”.
DeliveryCommand
Ce service se résume à un Worker, qui consomme les événements suivants :
- OrderCreated
- PaymentCreated
Celui-ci peut également émettre les événements suivants :
- OrderNotTreatable -> Si les articles ne sont plus disponibles
- OrderReadyForDelivery -> Les articles sont “réservés”, et la commande est prête à être livrée
- OrderSent
- OrderDelivered
Conclusion
Cette architecture reste relativement simple. En effet, elle permet de :
- Découper l’application en différents modules
- Effectuer des traitements asynchrones grâce au protocole AMQP
- Fournir une API unique pour les IHM ou services externes (ex: PayPal)
L’application est déjà plus maintenable, évolutive, et optimisable qu’une application monolithique. On peut ajouter autant de services qu’on le souhaite, sans alourdir les services existants. De plus, la scalabilité est améliorée, car on peut faire tourner 2 instances ou plus par service afin de fournir de la haute disponibilité, et traiter plus rapidement certaines requêtes. Un nouveau développeur entrant dans l’entreprise peut commencer son apprentissage du domaine étape par étape, en travaillant sur un seul service pour commencer. Et les équipes peuvent se répartir les services.
Le déploiement est plus compliqué, car nous avons plusieurs programmes à développer. Mais il peut être géré simplement grâce aux technologies de conteneurisation.
Le code lui est plus complexe, et le temps d’apprendre à travailler avec ces concepts peut entraîner des délais supplémentaires par rapport à une architecture monolithique. Néanmoins, une fois ces concepts maîtrisés, la différence devient négligeable (si on suit les bonnes pratiques de la programmation orientée objet).
Résumé
Architecture Monolithique
Un seul programme, un seul code.
Architecture Orientée Service
Application divisée en services, lesquels communiquent via des API (HTTP le plus souvent).
Architecture Orientée Événement
Application divisée en services, lesquels communiquent via un Message Broker, en produisant et en consommant des événements (messages).
Traitement Synchrone
Le client est bloqué tant que le traitement du serveur n’est pas terminé.
Traitement Asynchrone
Le client envoie sa requête au serveur, mais le traitement associé à cette requête est différé. Ainsi, le client n’a pas besoin d’attendre le résultat du traitement.
Shared Database
Une seule base de données partagée entre plusieurs applications.
Database per Service
Consiste à créer une base de données (voir serveur de données) propre à chaque application.
Saga
Permet de gérer un ensemble de transactions réparties sur plusieurs services, en orchestrant une série d’événements afin de faire passer un objet d’un état à un autre.
Aller au Chapitre 5 – Architecture Moderne : Exemple de création d’un nouveau projet