Pourquoi CQRS et Event Sourcing ne sont pas réservés à une élite ?
\n\nQuand on entend les termes "CQRS" ou "Event Sourcing", beaucoup de développeurs JavaScript imaginent quelque chose venant du monde de l'architecture d'entreprise en Java ou C#. Des schémas complexes, des événements infinis, des stockages étranges... Mais si je vous disais que ces patrons peuvent (et doivent !) être utilisés dans des applications Node.js classiques ?
\n\nDans cet article, nous allons voir comment CQRS et Event Sourcing aident à résoudre des problèmes réels : conflits de données, journalisation complexe, mise à l'échelle et débogage. Et tout cela, en pur JavaScript, sans magie ni frameworks complexes.
\n\nQu'est-ce que CQRS (Command Query Responsibility Segregation) ?
\n\nImaginez que votre application est un restaurant. Les serveurs (requêtes) et les cuisiniers (commandes) travaillent dans la même salle. Tôt ou tard, ils vont se gêner mutuellement. CQRS propose de séparer ces zones : les serveurs travaillent en salle (lecture), et les cuisiniers en cuisine (écriture).
\n\nCQRS est un principe de séparation des opérations en deux canaux :
\n- \n
- Commandes (Commands) — modifient l'état du système (Create, Update, Delete) \n
- Requêtes (Queries) — lisent uniquement les données (Read) \n
Dans une API REST classique, nous utilisons généralement le même objet pour la lecture et l'écriture. Par exemple, UserController contient les méthodes GET /users/:id et POST /users. CQRS propose de les répartir dans différentes couches.
Exemple simple de CQRS en JavaScript
\n\n// Commande (modification d'état)\nclass CreateUserCommand {\n constructor(name, email) {\n this.name = name;\n this.email = email;\n }\n}\n\nclass UserCommandHandler {\n async handle(command) {\n // Validation, logique métier, sauvegarde\n return await db.users.insert({\n name: command.name,\n email: command.email,\n created_at: new Date()\n });\n }\n}\n\n// Requête (lecture seule)\nclass GetUserQuery {\n constructor(userId) {\n this.userId = userId;\n }\n}\n\nclass UserQueryHandler {\n async handle(query) {\n // Lecture seule, aucune modification !\n return await db.users.findOne({ _id: query.userId });\n }\n}\n\nCela semble compliquer le code ? Oui, à première vue. Mais lorsque votre application atteindra 50+ points de terminaison, la séparation des commandes et des requêtes vous sauvera de :
\n- \n
- La modification accidentelle des données dans les requêtes GET \n
- Les problèmes de mise en cache (les requêtes sont faciles à mettre en cache, les commandes non) \n
- Les conflits lors du travail parallèle de plusieurs développeurs \n
Event Sourcing : mémoriser chaque action
\n\nImaginez maintenant qu'au lieu de stocker l'état actuel d'un utilisateur, vous stockez tous les événements qui lui sont arrivés. Pas "Ivan, 25 ans, Moscou", mais une séquence :
\n- \n
- "Utilisateur créé : Ivan" \n
- "Utilisateur a changé son âge à 25" \n
- "Utilisateur a déménagé à Moscou" \n
Event Sourcing est un patron où chaque modification d'état est sauvegardée comme un événement immuable. L'état actuel est calculé en "rejouant" tous les événements.
\n\nImplémentation d'un Event Store en JavaScript
\n\nclass EventStore {\n constructor() {\n this.events = [];\n }\n\n append(aggregateId, event) {\n this.events.push({\n aggregateId,\n event,\n timestamp: new Date()\n });\n }\n\n getEvents(aggregateId) {\n return this.events.filter(e => e.aggregateId === aggregateId);\n }\n}\n\n// Reconstruction de l'état à partir des événements\nfunction rebuildState(events) {\n return events.reduce((state, event) => {\n switch(event.event.type) {\n case 'USER_CREATED':\n return { ...state, name: event.event.name, age: 0 };\n case