Qu’est-ce qu’un monolithe modulaire
Un monolithe modulaire est une application unique déployable avec des frontières de modules internes strictes. De l’extérieur, il ressemble exactement à un monolithe : un seul processus, un seul build, une seule base de données, un seul déploiement. À l’intérieur, il est organisé en modules qui possèdent leurs propres données et exposent une interface publique restreinte, et ces frontières sont appliquées rigoureusement plutôt que d’être de simples intentions.
Il ne s’agit pas d’un compromis ou d’une étape transitoire par défaut. C’est une architecture légitime que de nombreux systèmes ne devraient jamais quitter. Elle conserve les propriétés qui facilitent la construction de logiciels — appels in-process, transaction unique, pipeline unique, refactorisations peu coûteuses — tout en ajoutant la discipline nécessaire pour éviter qu’une base de code en croissance ne devienne un plat de spaghettis.
La distinction cruciale se fait entre un seul élément déployable et un bloc non structuré. Un monolithe traditionnel en couches n’a aucune notion de propriété : n’importe quel contrôleur peut accéder à n’importe quelle table, et un changement se répercute sur l’ensemble de la base de code. Un monolithe modulaire stipule que le module de facturation est le seul à comprendre les factures, et que tous les autres communiquent avec lui via une porte qu’il contrôle.
Pourquoi c’est le choix par défaut le plus judicieux
La plupart des équipes se tournent vers les microservices pour résoudre des problèmes qu’elles n’ont pas. Un monolithe modulaire répond aux problèmes qu’elles rencontrent réellement — une base de code difficile à modifier, une propriété du code floue, des livraisons lentes — sans pour autant ajouter un réseau entre les différentes parties.
Les avantages pratiques sont considérables :
- Les appels internes sont gratuits. Lorsqu’un module appelle un autre module, il s’agit d’un appel de fonction, et non d’une requête avec un timeout, une politique de retry et un mode de défaillance.
- Les transactions sont réelles. Un cas d’utilisation qui touche un seul module est validé de manière atomique. Il n’y a pas de saga, pas de compensation et aucune fenêtre d’incohérence.
- Le refactoring se résume à un commit. Déplacer une frontière, renommer un concept ou fusionner deux modules est un travail ordinaire, et non une migration avec double écriture et basculement.
- L’exploitation reste simple. Un seul build, un seul déploiement, un seul ensemble de logs, une seule astreinte. Ce n’est pas un détail pour une équipe de cinq personnes.
- Le domaine a le temps de se stabiliser. Vous pouvez différer les décisions concernant les frontières jusqu’à ce que vous compreniez mieux le métier, au lieu de figer des suppositions dans l’infrastructure.
Le coût réel est que les modules ne tombent pas en panne et ne scalent pas indépendamment, et qu’un bug dans un module peut faire tomber l’ensemble de l’application. Pour la plupart des équipes et à la plupart des stades de développement d’un produit, c’est un compromis qui en vaut la peine.
Les modules suivent les capacités métier
Un module doit correspondre à une capacité métier (business capability), et non à une couche technique. La facturation, le catalogue, l’expédition, l’identité et les notifications sont des capacités. controllers, services, repositories et utils sont des couches ; les regrouper ainsi produit le monolithe monolithique classique où chaque modification de fonctionnalité nécessite de parcourir quatre répertoires.
Une bonne frontière de module possède les mêmes propriétés qu’une bonne frontière de service :
- Elle contient un ensemble cohérent de règles qui évoluent ensemble.
- Elle masque bien plus qu’elle n’expose.
- Elle peut être comprise par une seule équipe sans avoir à lire le reste du système.
Concrètement, chaque module est un dossier avec sa propre structure interne :
src/modules/billing/
domain/ # entities, value objects, invariants
invoice.ts
money.ts
application/ # use cases and orchestration
billing-service.ts
order-handlers.ts
infra/ # persistence and external clients
invoice-repository.ts
stripe-client.ts
index.ts # the only public entry point
Les dossiers domain et infra sont privés. index.ts ré-exporte le petit ensemble de types et de services que le reste de l’application est autorisé à utiliser. Ce fichier unique constitue le contrat du module, et il doit être suffisamment court pour être lu en une minute.
Si vous connaissez la règle de dépendance de la Clean Architecture, c’est la même idée appliquée au niveau du module : le domaine au centre ne dépend de rien à l’extérieur, et l’infrastructure dépend du domaine, et non l’inverse.
Le noyau partagé (shared kernel)
Certains concepts n’appartiennent réellement à aucun module spécifique. L’argent, un CustomerId, un Clock ou un type Result sont utilisés partout et n’appartiennent à personne. Placez-les dans un petit package shared explicite et gardez-le volontairement minimaliste.
src/shared/
money.ts # value object, no dependencies
ids.ts # branded id types
clock.ts # a testable time source
Un noyau partagé est un point de couplage, traitez donc sa croissance comme un signal d’alerte. Dès l’instant où shared contient un service, un repository ou quoi que ce soit qui connaît une règle métier, il devient un module sans propriétaire dont tous les autres modules dépendent. La règle d’or est que le dossier shared contient des types et des fonctions pures, jamais d’orchestration ou d’état.
Chaque module possède ses propres données
La règle qui confère toute sa puissance au monolithe modulaire est la propriété des données (data ownership). Chaque module est le seul autorisé à écrire dans ses tables ou son schéma, et aucun autre module ne peut lire ces tables directement. L’accès entre modules s’effectue via l’interface du module propriétaire ou par le biais d’un événement.
Dans une base de données unique, vous disposez de trois moyens pratiques pour exprimer cette propriété :
- Des schémas séparés.
billing.invoices,catalog.products,shipping.shipments. C’est le signal le plus clair, et cela facilite grandement une future scission de la base de données. - Des préfixes de table.
billing_invoices,catalog_products. Plus simple, mais avec la même intention. - Des bases de données séparées dès le départ. C’est la frontière la plus stricte, mais vous perdez l’avantage des transactions uniques entre modules et cela augmente la charge opérationnelle.
La plupart des monolithes modulaires devraient commencer avec une seule base de données et des schémas séparés. L’élément clé est la règle de propriété, et non la séparation physique : seul le repository du module propriétaire manipule ses tables. Si le module d’expédition a besoin de l’adresse d’un client, il interroge le module client ; il ne fait pas de SELECT depuis customers.
C’est précisément ce qui rend l’extraction possible par la suite. Lorsqu’un module possède déjà ses données et communique via une interface, le déplacer vers son propre service devient un simple changement de déploiement plutôt qu’une refonte complète.
Imposer les frontières grâce à l’outillage
Les frontières qui ne sont basées que sur des conventions finissent par s’estomper. Sous la pression des délais, importer le dépôt d’un autre module est toujours plus rapide que d’ajouter une méthode à son interface, et un raccourci en entraîne rapidement vingt autres. Rendez la frontière mécanique.
Une règle de dépendance est simple à énoncer et facile à vérifier : un module peut importer ses propres fichiers et l’interface publique d’autres modules, et rien d’autre. Des outils comme eslint-plugin-boundaries et dependency-cruiser permettent d’exprimer cela et de faire échouer le build en cas de non-respect.
{
"forbidden": [
{
"name": "no-cross-module-internals",
"from": { "path": "^src/modules/([^/]+)/" },
"to": {
"path": "^src/modules/(?!$1)([^/]+)/(?!index\\.ts).+"
}
},
{
"name": "no-cycles",
"from": {},
"to": { "circular": true }
}
]
}
Deux règles font l’essentiel du travail : interdire l’importation des composants internes d’un autre module et interdire les dépendances circulaires. Ajoutez une entrée CODEOWNERS par module afin que les revues de code soient assignées aux propriétaires du code, et traitez tout changement d’interface comme un petit changement d’API — cela mérite un second examen.
Communication inter-processus sans s’emmêler les pinceaux
Le mode de défaillance d’un monolithe est un graphe de dépendances où tout pointe vers tout. Un monolithe modulaire maintient ce graphe acyclique et peu profond. Il existe deux façons pour un module d’en utiliser un autre.
Appeler l’interface publique. Lorsque l’appelant a besoin d’une réponse immédiate, injectez le service de l’autre module et appelez-le. Orders appelle catalog.getProduct(sku) pour tarifer une ligne. L’appel est synchrone et s’effectue au sein du même processus, il est donc rapide et échoue via une exception, et non un timeout.
export class PlaceOrder {
constructor(
private readonly orders: OrderRepository,
private readonly catalog: CatalogService,
) {}
async execute(input: PlaceOrderInput): Promise<OrderId> {
const order = Order.create(input.customerId, input.lines);
for (const line of order.lines) {
const product = await this.catalog.getProduct(line.sku);
if (!product.isAvailable()) throw new OutOfStock(line.sku);
order.priceLine(line, product.priceCents);
}
await this.orders.save(order);
return order.id;
}
}
Publier un événement. Lorsque l’appelant n’a pas besoin de réponse, il annonce un fait et les autres modules réagissent. Orders publie order.placed ; billing, analytics et notifications s’y abonnent. Le module order ne sait pas qu’ils existent, ce qui permet de supprimer le couplage.
Les événements permettent également de briser les cycles. Si orders a besoin d’un effet de bord provenant de shipping et que shipping dépend déjà de orders, un appel direct créerait une boucle. Un événement permet à shipping de réagir sans que orders n’en dépende. Limitez le nombre d’appels directs entre modules ; si deux modules s’appellent constamment, ils forment probablement un seul et même module ou leur frontière est mal placée.
Une seule base de données, des schémas séparés
L’utilisation d’une base de données unique est un avantage, pas un compromis. Cela vous permet de bénéficier des transactions, des clés étrangères, d’un seul pool de connexions et d’un historique de migrations unique. Ce à quoi vous renoncez est l’isolation physique, que vous remplacez par la règle de propriété.
Au sein d’une même base de données, privilégiez un schéma par module. Cela permet de garder des noms de tables propres, rend la propriété visible dans chaque requête et offre à chaque module son propre espace de nom pour les migrations. Lorsqu’un module est extrait ultérieurement, son schéma suit le mouvement.
Évitez les clés étrangères entre modules. Une clé étrangère allant de billing.invoices vers catalog.products couple fortement les deux modules au niveau de la base de données : le catalogue ne peut être supprimé ou restructuré sans tenir compte de la facturation, et l’extraction nécessite la suppression de la contrainte. Stockez l’id et validez-le via l’interface à la place. Le guide PostgreSQL détaille les schémas et les contraintes.
Les migrations exigent la même discipline. Chaque module possède ses propres fichiers de migration, et le démarrage de l’application ou une étape de migration les applique dans l’ordre. Comme il n’y a qu’un seul déploiement, vous pouvez migrer et livrer simultanément, un luxe dont les microservices ne disposent pas.
Transactions et cohérence au sein d’un module
Le « super-pouvoir » de la transaction unique ne fonctionne que lorsqu’un cas d’utilisation reste confiné à un seul module. Faites en sorte que ce soit le cas le plus courant. Un cas d’utilisation qui charge un agrégat, valide un invariant et l’enregistre à nouveau doit constituer une seule transaction qui soit validée entièrement (commit) ou annulée proprement (rollback).
await db.transaction(async (tx) => {
const order = await orders.getForUpdate(orderId, tx);
order.confirm();
await orders.save(order, tx);
await outbox.add(
{ type: "order.confirmed", orderId: order.id },
tx,
);
});
Deux patterns permettent de maintenir la cohérence entre les modules.
L’outbox. Écrivez l’événement de domaine dans une table outbox au sein de la même transaction que le changement d’état, puis un dispatcher le publie ensuite. Cela garantit que l’événement n’est pas perdu si le processus plante entre le commit et la publication ; c’est d’ailleurs le même pattern que vous utiliseriez après une extraction.
Un process manager. Lorsqu’un flux s’étend sur plusieurs modules, un petit coordinateur peut écouter les événements et émettre la commande suivante, en gérant explicitement les tentatives (retries) et les timeouts. C’est l’équivalent interne d’une saga, et c’est bien plus simple qu’une saga distribuée car l’état de coordination réside dans une table classique.
N’essayez pas de mettre en œuvre une transaction distribuée. Si un cas d’utilisation s’étend réellement sur plusieurs modules et doit être atomique, c’est généralement le signe que ces modules ne forment en réalité qu’un seul et même module.
Le chemin vers l’extraction
L’intérêt d’investir dans des frontières claires est l’optionalité. Un monolithe modulaire dont les modules communiquent via des interfaces et des événements peut être décomposé ultérieurement, module par module, en utilisant l’approche du strangler fig.
- Choisissez le module ayant la frontière la plus nette et la pression la plus forte. Un besoin de mise à l’échelle indépendante, le rythme de travail d’une équipe distincte ou une exigence de conformité sont de bonnes raisons.
- Confirmez qu’il possède ses propres données. Si d’autres modules lisent encore ses tables, corrigez cela d’abord en les redirigeant via l’interface.
- Attribuez-lui sa propre base de données et son propre pipeline. Déplacez son schéma, pointez son repository vers le nouveau stockage et maintenez l’interface stable.
- Remplacez les appels internes (in-process) par des appels réseau ou des événements. Les appelants dépendent déjà d’une interface, il s’agit donc d’un changement d’adaptateur et non d’une réécriture.
- Remplacez le dispatcher d’événements par un broker. Grâce au pattern outbox, le flux d’événements change à peine lorsque le transport devient Kafka ou RabbitMQ.
Comme la couture existe déjà, chaque étape est délimitée. C’est l’argument le plus fort en faveur du monolithe modulaire : ce n’est pas une destination différente des microservices, c’est l’option d’y parvenir délibérément, uniquement pour les parties qui le justifient.
Tester les modules en isolation
Les frontières de modules sont un atout majeur pour les tests. Puisqu’un module expose une interface publique et possède ses propres données, vous pouvez le tester indépendamment sans avoir à démarrer l’application complète.
- Les tests de domaine sont purs et rapides. Instanciez l’agrégat, exercez les règles métier et vérifiez le résultat. Pas de base de données, pas de HTTP.
- Les tests d’interface de module pilotent le service public du module contre une base de données de test limitée à son propre schéma. Ils vérifient le contrat dont dépendent les autres modules.
- Les tests de contrat d’événements vérifient que le module publie les événements attendus par les consommateurs, avec les champs sur lesquels ils s’appuient.
- Les tests de bout en bout (end-to-end) sollicitent la couche HTTP pour un petit nombre de flux critiques. Limitez-les, car ils sont lents.
L’alternative basée sur les couches force chaque test significatif à traverser toutes les couches, c’est pourquoi ces suites de tests deviennent lentes et instables. Tester à la frontière du module permet de garder la majorité des tests rapides tout en protégeant les interfaces essentielles.
Monolithe modulaire versus monolithe en couches
Il est important d’être précis, car les deux sont des « monolithes », mais un seul est modulaire.
| Monolithe en couches | Monolithe modulaire | |
|---|---|---|
| Groupement | Par couche technique | Par domaine métier |
| Impact d’un changement | S’étend à toutes les couches | Reste confiné à un module |
| Accès aux données | Toute couche accède à n’importe quelle table | Le module possède ses propres tables |
| Propriété (Ownership) | Floue | Une équipe par module |
| Testabilité | Prédominance des tests de bout en bout | Tests limités au périmètre du module |
| Extraction | Nécessite une réécriture | Changement délimité |
Le monolithe en couches n’est pas un mauvais choix pour une petite application ; il est simple et familier. Il devient cependant un handicap lorsque plusieurs personnes y travaillent, car il n’existe aucune jointure permettant de diviser le travail, ni aucun moyen de raisonner sur un changement de manière locale.
Le mode de défaillance : la “boule de boue” (big ball of mud)
Le monolithe modulaire échoue d’une manière bien précise : la dissolution des frontières. Tout commence par un raccourci raisonnable qui finit par devenir la norme.
Les signes d’alerte :
- Un module importe le dossier
infrad’un autre module. - Deux modules écrivent dans la même table.
- Un package
utilsousharedse transforme en une seconde application. - La modification du schéma d’un module fait échouer le build d’un autre module.
- Le graphe de dépendances présente un cycle, généralement introduit par un unique import “juste pour cette fois”.
La prévention repose sur la même discipline que le reste de l’architecture : des règles de lint qui font échouer le build, des code owners par module, une interface publique restreinte et une culture de revue de code qui traite tout import interne comme un bug. Les frontières sont peu coûteuses à maintenir mais très coûteuses à rétablir ; imposez-les donc dès le premier module.
Événements au sein d’un même processus
Les événements in-process permettent de découpler les modules sans avoir recours à un broker. Un petit dispatcher reçoit un fait publié et appelle les handlers abonnés, le tout au sein du même processus et, si vous le souhaitez, de la même transaction.
type DomainEvent = { type: string; occurredAt: string };
type Handler = (event: DomainEvent, tx?: Transaction) => Promise<void>;
class EventBus {
private handlers = new Map<string, Handler[]>();
on(type: string, handler: Handler) {
this.handlers.set(type, [...(this.handlers.get(type) ?? []), handler]);
}
async publish(event: DomainEvent, tx?: Transaction) {
for (const handler of this.handlers.get(event.type) ?? []) {
await handler(event, tx);
}
}
}
Deux points de vigilance. Si les handlers s’exécutent dans la transaction de l’appelant, un handler lent prolongera la transaction et tout échec entraînera le rollback de l’ensemble du cas d’utilisation. S’ils s’exécutent après le commit, un crash entre les deux peut entraîner la perte de l’événement. Le pattern outbox résout ce problème en écrivant l’événement dans une table outbox au sein de la même transaction, pour ensuite effectuer le dispatch depuis celle-ci.
await db.transaction(async (tx) => {
await orders.save(order, tx);
await tx.insert(outbox).values({
type: "order.placed",
payload: order.toEvent(),
});
});
Comme l’événement est commit avec l’état, il ne peut pas être perdu, et un relay peut retenter le dispatch jusqu’à ce que chaque abonné l’ait traité. Lorsqu’un module est extrait ultérieurement, le relay est la seule chose qui change.
Un gestionnaire de processus pour les flux inter-modules
Lorsqu’un cas d’utilisation s’étend sur plusieurs modules et doit réagir aux échecs, un process manager le coordonne explicitement au lieu de masquer le flux dans une chaîne de gestionnaires d’événements. Il écoute les événements, conserve son propre état et émet des commandes.
class PlaceOrderProcess {
async onOrderPlaced(event: OrderPlaced) {
await this.catalog.reserve(event.orderId, event.lines);
}
async onReservationFailed(event: ReservationFailed) {
await this.orders.cancel(event.orderId, "out_of_stock");
await this.notifications.send(event.customerId, "order_cancelled");
}
async onReservationConfirmed(event: ReservationConfirmed) {
await this.payments.charge(event.orderId, event.totalCents);
}
}
Le process manager est l’équivalent interne d’une saga. Il rend le chemin nominal (happy path) et le chemin de compensation visibles en un seul endroit, ce que la chorégraphie a tendance à masquer. Conservez l’état du processus dans une table classique afin qu’un redémarrage puisse reprendre là où il s’était arrêté.
Versionner l’interface d’un module
L’index.ts d’un module est une API interne et mérite autant de soin qu’une API publique. D’autres modules compilent en s’appuyant dessus, donc un changement imprudent peut casser leurs builds.
- Ajoutez, ne cassez pas. Ajoutez des paramètres optionnels et de nouvelles méthodes ; évitez de modifier une signature existante.
- Gardez une surface réduite. Chaque export est une promesse. Si un type n’a pas besoin de sortir du module, ne l’exportez pas.
- Dépréciez par étapes. Marquez l’ancienne méthode, migrez les appels dans des commits séparés, puis supprimez-la. Comme il s’agit d’une seule base de code, vous pouvez rechercher chaque appelant.
- Testez le contrat. Les tests d’interface de module protègent la promesse que vous avez faite aux autres modules.
C’est moins coûteux que le versionnement d’une API réseau car il n’y a pas d’ordre de déploiement à respecter, mais la discipline est la même, et c’est ce qui évite qu’une future extraction ne se transforme en réécriture complète.
Migrations par module
Comme un monolithe modulaire utilise une seule base de données, il est tentant de conserver un ensemble unique et géant de fichiers de migration. Cela recréerait un couplage que l’architecture tente justement de supprimer. Au lieu de cela, laissez chaque module gérer ses propres migrations, limitées à son schéma ou à son préfixe de table.
migrations/
catalog/ 20260901_add_product_status.sql
orders/ 20260903_add_order_confirmed_at.sql
billing/ 20260905_add_invoice_paid_at.sql
Une migration qui modifie les tables d’un autre module est une violation de frontière déguisée et devrait être refusée lors de la revue de code. Maintenir des migrations modulaires signifie que la règle de propriété s’applique jusqu’au schéma, et cela rend le transfert des tables d’un module vers sa propre base de données aussi simple que de rejouer le contenu d’un seul dossier.
Quand fusionner à nouveau des modules
Toutes les frontières ne sont pas correctes dès le départ. Si deux modules changent systématiquement ensemble, partagent une transaction et s’appellent mutuellement dans les deux sens, ils ne forment en réalité qu’un seul module avec une coupure artificielle. Les fusionner à nouveau est une décision légitime et saine.
Les signes sont concrets : une pull request modifie régulièrement les deux modules, leur interface change à chaque nouvelle fonctionnalité, et le graphe de dépendances présente un cycle que vous devez constamment contourner. La fusion est simple dans un monolithe — déplacez le code, supprimez l’interface, mettez à jour les imports — et cela élimine un coût de coordination qui ne ferait que s’aggraver. L’objectif est d’avoir des frontières claires, et non un nombre précis de modules.
Maintenir la santé des jointures
Les frontières se dégradent silencieusement ; il est donc préférable de les vérifier régulièrement plutôt que d’attendre une réécriture complète. Quelques fitness functions automatisées permettent de détecter cette dérive tant qu’elle est encore peu coûteuse à corriger.
- Faire échouer la CI lorsqu’un module importe les composants internes d’un autre module.
- Faire échouer la CI lorsque le graphe de dépendances contient un cycle.
- Faire échouer la CI lorsqu’une migration fait référence à une table appartenant à un autre module.
- Suivre l’évolution du nombre de fichiers et d’exports publics par module ; un module qui ne cesse de croître est peut-être une frontière mal placée.
- Exiger une revue
CODEOWNERSpour tout changement apporté à l’interface publique d’un module.
Aucune de ces mesures ne nécessite d’infrastructure nouvelle. Ce sont des tests et des règles de lint classiques qui, ensemble, transforment l’accord tacite « nous avons convenu de respecter les frontières » en une contrainte appliquée par le build.
Bonnes pratiques
- Définissez vos modules par capacité métier, et jamais par couche technique.
- Attribuez à chaque module un unique fichier
indexpublic et gardez-le concis. - Faites en sorte que chaque module soit le seul habilité à écrire dans ses propres tables ou son propre schéma.
- Interdisez les imports d’internals entre modules ainsi que les dépendances cycliques via des règles de lint en CI.
- Privilégiez l’appel direct à une interface lorsque vous avez besoin d’une réponse, et un événement dans le cas contraire.
- Maintenez un graphe de dépendances acyclique et peu profond ; si deux modules s’appellent mutuellement, fusionnez-les ou redéfinissez leurs frontières.
- Utilisez une seule base de données avec un schéma par module et évitez les clés étrangères entre modules.
- Gardez un cas d’utilisation au sein d’un seul module afin qu’il tienne dans une seule transaction.
- Utilisez un outbox pour assurer la cohérence entre modules plutôt que des transactions distribuées.
- Testez les modules via leur interface publique, avec quelques tests de bout en bout aux extrémités.
- Concevez vos interfaces pour qu’elles soient compatibles avec les événements, afin qu’un module puisse être extrait sans nécessiter de réécriture.
Erreurs courantes
- Parler de monolithe modulaire tout en partageant des tables et en important des composants internes.
- Organiser les dossiers par couche en croyant que les modules sont réellement isolés.
- Ignorer les règles de lint sous prétexte que « tout le monde connaît la convention ».
- Laisser un package d’utilitaires partagés devenir un point de couplage caché.
- Exécuter un flux transverse aux modules comme une transaction distribuée alors que les modules devraient n’en former qu’un seul.
- Introduire des dépendances circulaires et tenter de les masquer avec des imports dynamiques.
- Extraire un service avant que le module n’ait une interface propre ou ne possède ses propres données.
- Placer les règles métier dans les controllers, empêchant ainsi de tester les modules de manière isolée.
- Considérer le style comme une solution temporaire et ne jamais faire respecter les frontières.
- Supposer qu’un déploiement unique signifie un domaine de panne unique et négliger le travail de résilience de base.
Et après ?
Si une pression concrète justifie plus tard un découpage, le guide sur les Microservices explique les gains, les coûts et la manière d’extraire un module à la fois. Pour organiser le code à l’intérieur de chaque module, consultez Clean Architecture, et pour découpler les modules via des faits plutôt que des appels, lisez Event-Driven Architecture. Lorsque vous avez besoin des modèles de stockage liés à la propriété des modules, la section PostgreSQL traite des schemas, des transactions et des contraintes.