System Design

Démystifier CQRS : Faire évoluer votre système avec la séparation des responsabilités Commande-Requête

Dans le monde des applications à haut débit et à grande échelle, les architectures CRUD (Créer, Lire, Mettre à jour, Supprimer) traditionnelles butent souvent sur une limite. Le modèle principal, bien que simple, force les opérations de lecture et d'écriture à partager la même structure de données et les mêmes contraintes d'optimisation. La séparation des responsabilités Commande-Requête (CQRS) est un motif qui résout ce problème en séparant les côtés lecture et écriture d'une application en modèles distincts. Cette approche vous permet d'optimiser chaque côté indépendamment, ce qui conduit à de meilleures performances, une meilleure évolutivité et une meilleure maintenabilité.

Qu'est-ce que CQRS ?

CQRS est un principe d'architecture logicielle qui divise le modèle de données en deux : un modèle de Commande pour gérer les écritures (mutations) et un modèle de Requête pour gérer les lectures (récupérations). L'idée centrale est simple : les opérations qui modifient l'état (Commandes) ne doivent pas retourner de données, et les opérations qui récupèrent des données (Requêtes) ne doivent pas modifier l'état.

Cette séparation permet aux côtés lecture et écriture d'avoir des schémas de base de données, des technologies de stockage ou même des infrastructures d'hébergement différentes, adaptés spécifiquement à leurs cas d'utilisation.

Pourquoi utiliser CQRS ?

  • Optimisation des performances : Vous pouvez dénormaliser le modèle de lecture pour des requêtes rapides sans affecter l'intégrité du modèle d'écriture.
  • Évolutivité : Les applications intensives en lecture peuvent étendre le côté lecture indépendamment du côté écriture.
  • Sécurité : Les opérations d'écriture peuvent être strictement contrôlées et auditées, tandis que les opérations de lecture restent ouvertes et rapides.
  • Gestion de la complexité : Dans les domaines complexes, la séparation des préoccupations empêche l'anti-motif du « Modèle Dieu » où une seule entité géante gère tout.

Aperçu de l'architecture

Dans une implémentation CQRS typique, le flux ressemble à ceci :

  1. Une Commande est envoyée au côté Commande.
  2. Le côté Commande valide et traite la logique métier, en persistant les événements dans un Stockage d'Écriture.
  3. Les événements sont publiés sur un Bus d'Événements ou une File de Messages.
  4. Les gestionnaires du côté Requête consomment ces événements et mettent à jour le Stockage de Lecture (souvent une base de données NoSQL ou un index de recherche).
  5. Une Requête est envoyée au côté Requête, qui récupère des données pré-calculées depuis le Stockage de Lecture.

Exemple de code

Ci-dessous, un exemple simplifié en C# illustrant la séparation des Commandes et des Requêtes.

// 1. Définir une Commande (Opération d'écriture)
public record CreateOrderCommand : ICommand<Guid>
{
    public Guid CustomerId { get; init; }
    public decimal Amount { get; init; }
}

// 2. Définir une Requête (Opération de lecture)
public record GetOrderByIdQuery : IQuery<OrderDto>
{
    public Guid OrderId { get; init; }
}

// 3. Gestionnaire de Commande (Écrit dans la source de vérité)
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid>
{
    private readonly IOrderRepository _repo;
    private readonly IEventPublisher _publisher;

    public async Task<Guid> Handle(CreateOrderCommand command, CancellationToken ct)
    {
        // Logique métier et validation
        var order = new Order(command.CustomerId, command.Amount);
        
        // Persister dans le Stockage d'Écriture (BDD relationnelle)
        await _repo.SaveAsync(order, ct);
        
        // Publier l'Événement de Domaine
        await _publisher.PublishAsync(new OrderCreatedEvent(order.Id), ct);
        
        return order.Id;
    }
}

// 4. Gestionnaire de Requête (Lit depuis le stockage optimisé)
public class GetOrderByIdQueryHandler : IQueryHandler<GetOrderByIdQuery, OrderDto>
{
    private readonly IOrderReadRepository _readRepo;

    public async Task<OrderDto> Handle(GetOrderByIdQuery query, CancellationToken ct)
    {
        // Lire depuis NoSQL/ES/Cache
        return await _readRepo.GetByIdAsync(query.OrderId, ct);
    }
}

Considérations pratiques

Bien que CQRS soit puissant, il introduit de la complexité. Vous devez gérer la cohérence éventuelle, car le côté lecture peut être en retard par rapport au côté écriture. De plus, le maintien de la synchronisation entre les modèles d'écriture et de lecture nécessite un traitement des événements robuste et une surveillance. Il est recommandé de commencer CQRS avec une approche de « séparation Lecture-Écriture » où vous conservez la même base de données mais avec des vues différentes, avant de passer à des stockages de données entièrement séparés.

Conclusion

CQRS n'est pas une solution miracle pour chaque application. Il est le mieux adapté aux systèmes avec un haut débit d'écriture, une logique métier complexe ou des exigences spécifiques de performance de lecture. En séparant les commandes et les requêtes, vous gagnez la flexibilité de faire évoluer et d'optimiser les composants de votre système indépendamment. À mesure que votre application gagne en complexité, l'adoption de CQRS peut être un coup stratégique vers une architecture plus robuste et évolutive.

Share: