Dans le paysage moderne de l'ingénierie logicielle, construire des systèmes capables de gérer des millions d'utilisateurs simultanés tout en maintenant la cohérence des données n'est plus un luxe, mais une nécessité. Les architectures CRUD (Create, Read, Update, Delete) traditionnelles peinent souvent sous un débit élevé, ce qui entraîne des conflits de base de données, une gestion de transaction complexe et des problèmes d'état difficiles à déboguer. Pour surmonter ces goulets d'étranglement, les équipes avancées se tournent de plus en plus vers une combinaison de la Ségrégation des Responsabilités de Commande et de Requête (CQRS) et de l'Event Sourcing.
Comprendre les concepts fondamentaux
Le CQRS et l'Event Sourcing sont souvent mentionnés ensemble, mais ils répondent à des préoccupations différentes. Le CQRS est un modèle architectural qui sépare les opérations de lecture et d'écriture en des modèles distincts. Dans un système traditionnel, une seule base de données gère à la fois les requêtes et les mises à jour. Avec le CQRS, les "Commandes" modifient l'état (côté écriture), tandis que les "Requêtes" récupèrent l'état (côté lecture). Cette séparation permet aux équipes de mettre à l'échelle les lectures et les écritures indépendamment et d'optimiser chaque modèle pour son cas d'utilisation spécifique.
L'Event Sourcing pousse cela plus loin du côté écriture. Au lieu de stocker l'état actuel d'une entité, l'event sourcing stocke une séquence d'événements immuables qui représentent les modifications apportées à cette entité. L'état actuel n'est pas stocké directement ; il est dérivé en rejouant ces événements. Cela fournit un journal d'audit complet, simplifie les requêtes temporelles (savoir quel était l'état à un moment donné) et découple le stockage des données de la logique métier.
Pourquoi choisir cette architecture pour un haut débit ?
Pour les systèmes distribués à haut débit, les principaux avantages sont la performance et la résilience. En séparant les chemins de lecture et d'écriture, vous empêchez les charges de travail lourdes en lecture de bloquer les opérations d'écriture. De plus, comme les événements sont ajoutés de manière séquentielle et immuables, ils peuvent être écrits dans des systèmes de journaux haute performance (comme Apache Kafka ou AWS Kinesis) plutôt que dans des bases de données relationnelles traditionnelles, augmentant ainsi considérablement le débit d'écriture.
Prenons l'exemple d'une plateforme de trading financier. Chaque exécution de trade est un événement. En stockant ces événements, vous satisfaites non seulement la conformité réglementaire en conservant un historique parfait, mais vous permettez également de reconstruire la valeur du portefeuille à n'importe quelle milliseconde spécifique sans tables historiques complexes.
Exemple d'implémentation : Un service de commande simple
Examinons une implémentation conceptuelle dans un domaine simplifié. Nous définirons un événement et une commande pour illustrer le flux.
// Définir l'événement immuable
class OrderCreatedEvent {
constructor(orderId, customerId, items) {
this.orderId = orderId;
this.customerId = customerId;
this.items = items;
this.timestamp = new Date();
}
}
// Définir la commande
class PlaceOrderCommand {
constructor(userId, orderDetails) {
this.userId = userId;
this.orderDetails = orderDetails;
}
}
// L'agrégat racine gère la commande et produit des événements
class OrderAggregate {
constructor() {
this.events = [];
}
// Appliquer la commande
async placeOrder(command) {
// 1. Valider les règles métier
if (command.orderDetails.items.length === 0) {
throw new Error("La commande ne peut pas être vide");
}
// 2. Créer l'événement
const event = new OrderCreatedEvent(
generateId(),
command.userId,
command.orderDetails.items
);
// 3. Appliquer l'événement à l'état actuel
this.applyEvent(event);
// 4. Sauvegarder l'événement dans le magasin d'événements (Côté écriture)
await eventStore.save([event]);
}
// Appliquer l'événement à l'état interne
applyEvent(event) {
if (event instanceof OrderCreatedEvent) {
this.id = event.orderId;
this.customerId = event.customerId;
this.items = event.items;
}
}
}
Reconstruire l'état pour les lectures
Côté lecture, un moteur de projection séparé écoute le flux d'événements. Il consomme ces événements et met à jour des modèles de lecture dénormalisés (tels qu'Elasticsearch ou une vue matérialisée dans SQL) optimisés pour des requêtes rapides. Cela garantit que les réponses de votre API sont instantanées, même lorsque la charge d'écriture augmente.
// Gestionnaire de projection (Côté lecture)
class OrderProjection {
constructor(readDb) {
this.readDb = readDb;
}
handleEvent(event) {
if (event.type === 'ORDER_CREATED') {
// Insérer dans une table dénormalisée pour une lecture rapide
this.readDb.insert({
orderId: event.payload.orderId,
customerId: event.payload.customerId,
itemsCount: event.payload.items.length,
createdAt: event.payload.timestamp
});
}
}
}
Conclusion
Implémenter l'Event Sourcing et le CQRS n'est pas une solution miracle ; cela introduit une complexité concernant le versionnement des événements, les éventualités de cohérence et la charge opérationnelle. Cependant, pour les systèmes distribués à haut débit où l'évolutivité, l'auditabilité et la performance sont primordiales, les avantages l'emportent largement sur les coûts. En maîtrisant ces modèles, vous pouvez construire des systèmes qui sont non seulement résilients, mais aussi capables d'évoluer avec les exigences croissantes de vos utilisateurs.