Dans beaucoup d’applications métier, on finit par mélanger dans les mêmes modèles et les mêmes services des besoins très différents : d’un côté des commandes qui modifient l’état, de l’autre des lectures optimisées pour les écrans et les rapports. Le pattern CQRS (Command Query Responsibility Segregation) propose de séparer explicitement écriture et lecture, afin d’avoir un modèle riche et cohérent pour les règles métier, et un modèle de lecture simple, rapide et taillé pour les besoins réels de consultation.
Introduction à CQRS
Idée de base
Command: une action qui modifie l’état du système (créer une commande, changer un statut, etc.).Query: une action qui lit les données sans les modifier (liste des commandes, détails d’un client, etc.).- On n’utilise pas le même modèle ni parfois la même base de données pour écrire et pour lire.
Architecture simplifiée
- Côté écriture :
- Commandes → Handlers de commandes → Domaine (agrégats, règles métier) → Base “d’écriture”.
- Côté lecture :
- Événements émis lors des écritures → Projections / vues matérialisées → Base “de lecture” optimisée pour les requêtes (tables dénormalisées, vues, index, etc.).
Pourquoi l’utiliser ?
- Permet un modèle riche côté écriture (règles métier) et un modèle simple/rapide côté lecture.
- Facilite la montée en charge : on peut scaler séparément lecture et écriture.
- S’intègre souvent avec Event Sourcing (les écritures produisent des événements qui servent à reconstruire les lectures).
Inconvénients
- Complexité accrue (deux modèles, synchronisation des projections).
- Latence éventuelle entre une écriture et la mise à jour du modèle de lecture (consistance éventuelle, pas toujours immédiate).
CQRS, cache et agrégation de données
1. Pourquoi CQRS facilite la mise en cache
- Un côté lecture “pur”, sans effets de bord
- Les queries ne modifient jamais l’état : ce sont des fonctions “read-only”.
- Cela rend la mise en cache beaucoup plus sûre (moins de risque de cacher un résultat qui dépendrait d’un effet de bord).
-
Un modèle de lecture optimisé pour le cache
- Le modèle de lecture peut être conçu dès le départ comme “cache‑friendly” :
- DTO simples, facilement sérialisables (JSON, MessagePack, etc.),
- structures adaptées à Redis, Memcached, CDN, etc.
- Il n’est pas nécessaire de mettre en cache l’agrégat métier complet : seules les vues strictement requises par les écrans sont conservées en cache.
- Le modèle de lecture peut être conçu dès le départ comme “cache‑friendly” :
-
Invalidation centralisée côté écriture
- L’ensemble des modifications transite par les commandes et handlers d’écriture.
- Ce point de passage unique permet :
- d’invalider ou de rafraîchir de manière ciblée les entrées de cache concernées,
- ou de publier un événement déclenchant la mise à jour des vues de lecture et du cache.
→ Le flux “écriture → projections → cache” est ainsi explicite et donc plus aisément maîtrisable.
- Scalabilité indépendante
- Le modèle de lecture et son cache peuvent être mis à l’échelle indépendamment de la base d’écriture :
- plusieurs instances de services de lecture derrière un cache partagé,
- edge caching pour des endpoints strictement “query”.
- CQRS rend naturelle la mise en place d’un pipeline spécifiquement dédié à l’optimisation de la lecture, dont le cache constitue un élément clé.
- Le modèle de lecture et son cache peuvent être mis à l’échelle indépendamment de la base d’écriture :
2. CQRS et agrégation de données
-
Projections pré-agrégées
- Lors des écritures, vous publiez des événements (par ex.
OrderPlaced,PaymentCompleted). - Des projections consomment ces événements et maintiennent des tables ou vues agrégées :
- total des ventes par jour,
- nombre de commandes par statut,
- nombre d’utilisateurs actifs par région, etc.
- Les queries lisent directement ces vues agrégées : il n’est plus nécessaire de recalculer l’agrégat à la volée sur des tables brutes coûteuses.
- Lors des écritures, vous publiez des événements (par ex.
-
Agrégations sur mesure pour chaque usage
- Vous pouvez définir plusieurs modèles de lecture spécialisés :
- une projection pour les KPI d’un “dashboard temps réel”,
- une autre pour le reporting métier,
- une autre pour la recherche ou l’analytics.
- Chacun de ces modèles est alimenté par le flux d’événements, sans impacter la structure du modèle d’écriture.
- Vous pouvez définir plusieurs modèles de lecture spécialisés :
-
Découplage entre modèle transactionnel et modèle analytique léger
- Le modèle d’écriture reste normalisé et cohérent pour les transactions.
- Les agrégations sont maintenues dans des structures séparées (vues matérialisées, tables dénormalisées, index agrégés), éventuellement dans une autre base (moteur OLAP léger, base de séries temporelles, etc.).
- CQRS formalise ce découplage au niveau architectural, plutôt que de le laisser au stade de simples optimisations SQL ponctuelles.
3. Mise en cache des agrégats de lecture
-
Agrégations pré‑calculées + cache = réponses quasi instantanées
- Les agrégations sont pré‑calculées via les projections.
- Elles sont exposées via l’API de lecture, puis mises en cache (ou bien la table de projection elle‑même joue le rôle de “vue matérialisée en cache” dans une base en mémoire).
- L’API se limite alors à :
- effectuer une simple recherche,
- éventuellement appliquer quelques filtres,
- retourner la réponse.
-
Stratégies de cache pilotées par le domaine
- Chaque événement métier (commande payée, utilisateur inscrit, etc.) peut entraîner :
- une mise à jour atomique de la projection d’agrégation,
- puis un rafraîchissement ciblé des clés de cache concernées.
- Au lieu d’invalidations globales (vidage complet du cache), on adopte des stratégies fines et déterministes fondées sur le domaine.
- Chaque événement métier (commande payée, utilisateur inscrit, etc.) peut entraîner :
4. Consistance éventuelle assumée et contrôlée
-
CQRS + cache = consistance éventuelle explicite
- Les projections agrégées et le cache ne reflètent pas nécessairement l’état exact, à l’instant T, de la base d’écriture.
- CQRS conduit à définir explicitement :
- le moment et les modalités de mise à jour des projections,
- la latence acceptable pour chaque vue (tableau de bord quasi temps réel vs rapport journalier).
- Cette consistance éventuelle est encadrée par un workflow d’événements, des files de messages et des délais maîtrisés.
-
Adaptation en fonction du type de vue
- Pour certaines lectures critiques, il est possible de :
- contourner le cache,
- lire directement la source d’écriture ou une projection synchronisée plus strictement.
- Pour les agrégats coûteux mais tolérants à un certain délai, le pipeline “écriture → agrégation → cache” est exploité au maximum.
- Pour certaines lectures critiques, il est possible de :
Exemple d’implementation CQRS simple en C
Voici un exemple minimal en .NET (C#) illustrant CQRS sans recours à un framework externe (type MediatR), comprenant :
- une commande : création d’une commande,
- une requête : lecture du détail d’une commande,
- un “bus” léger de commandes/requêtes,
- un modèle d’écriture (agrégat
Order) et un modèle de lecture (OrderReadModel).
L’ensemble du code peut être placé dans un seul fichier Program.cs d’une application console.
1. Contrats
Cette section définit les « contrats » de base pour mettre en place le pattern CQRS (Command Query Responsibility Segregation). L’idée de CQRS est de séparer clairement les opérations d’écriture (commandes) des opérations de lecture (requêtes), afin de structurer le code autour de ces deux responsabilités distinctes.
L’interface ICommand marque un type comme étant une commande : c’est une intention de modifier l’état du système (créer, mettre à jour, supprimer des données, lancer un processus, etc.). Elle ne retourne rien en elle‑même ; son rôle est surtout d’offrir un point commun à toutes les commandes, ce qui permet de les manipuler de façon générique (par exemple via un bus de commandes).
L’interface générique IQuery<TResult> représente une requête de lecture qui ne modifie pas l’état, mais retourne un résultat de type TResult. Chaque requête décrit les critères de lecture (filtres, identifiants, pagination, etc.), et le type générique exprime clairement la forme du résultat attendu.
Les interfaces ICommandHandler<TCommand>, ICommandHandler<TCommand, TResult> et IQueryHandler<TQuery, TResult> définissent les « handlers » responsables de l’exécution de ces commandes et requêtes. Un ICommandHandler<TCommand> expose une méthode asynchrone Handle qui prend une commande et effectue l’action correspondante, sans valeur de retour. Lorsque l’on souhaite récupérer une valeur (par exemple l’identifiant de la ressource créée), on peut utiliser ICommandHandler<TCommand, TResult>, dont la méthode Handle renvoie un Task<TResult>. Dans les deux cas, la contrainte where TCommand : ICommand garantit que le type manipulé est bien une commande. De façon similaire, IQueryHandler<TQuery, TResult> expose une méthode asynchrone Handle qui reçoit une requête TQuery et renvoie un résultat Task<TResult>. La contrainte where TQuery : IQuery<TResult> assure que la requête traitée est cohérente avec le type de résultat attendu.
En résumé, ce bloc de code pose la fondation contractuelle : il sépare les intentions (commandes et requêtes) de leur exécution (handlers), favorisant une architecture claire, testable et extensible autour de CQRS.
#region Contrats CQRS
// Commandes = écriture
public interface ICommand { }
// Requêtes = lecture
public interface IQuery<TResult> { }
// Handlers de commande
public interface ICommandHandler<TCommand> where TCommand : ICommand
{
Task Handle(TCommand command);
}
// Handlers de commande avec valeur de retour
public interface ICommandHandler<TCommand, TResult> where TCommand : ICommand
{
Task<TResult> Handle(TCommand command);
}
// Handlers de requête
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
Task<TResult> Handle(TQuery query);
}
#endregion
2. Requête
Ce code illustre la partie « lecture » (Query) de l’architecture CQRS pour récupérer le détail d’une commande.
Le record GetOrderDetailsQuery implémente IQuery<OrderReadModel?>. Il représente une requête de lecture qui demande les détails d’une commande identifiée par son OrderId. Le type générique OrderReadModel? indique que la requête renverra soit un modèle de lecture de commande, soit null si la commande n’existe pas.
La classe GetOrderDetailsQueryHandler implémente IQueryHandler<GetOrderDetailsQuery, OrderReadModel?>. Elle dépend de IOrderReadModelStore, une abstraction du stockage du modèle de lecture des commandes, injectée via le constructeur. La méthode Handle effectue une lecture pure : elle ne touche pas au domaine d’écriture, mais interroge directement le store de modèles de lecture avec _readModels.GetById(query.OrderId). Le handler se contente ainsi de projeter la requête vers la source de données optimisée pour la lecture, conformément aux principes de CQRS.
#region Query + Handler (lecture)
public record GetOrderDetailsQuery(Guid OrderId) : IQuery<OrderReadModel?>;
public class GetOrderDetailsQueryHandler : IQueryHandler<GetOrderDetailsQuery, OrderReadModel?>
{
private readonly IOrderReadModelStore _readModels;
public GetOrderDetailsQueryHandler(IOrderReadModelStore readModels)
{
_readModels = readModels;
}
public Task<OrderReadModel?> Handle(GetOrderDetailsQuery query)
{
// Lecture pure sur le modèle de lecture
return _readModels.GetById(query.OrderId);
}
}
#endregion
3. Commande
Ce code illustre une commande concrète de création de commande et son handler côté écriture dans une architecture CQRS.
Le type CreateOrderCommand est un record qui implémente ICommand. Il représente l’intention de créer une nouvelle commande pour un client donné, identifié par son CustomerId. En tant que simple objet de données, il ne contient pas de logique métier ; il transporte uniquement les informations nécessaires pour exécuter l’action.
La classe CreateOrderCommandHandler implémente ICommandHandler<CreateOrderCommand, Guid> : c’est elle qui exécute réellement la logique métier associée à cette commande et renvoie l’identifiant (Guid) de la commande créée. Elle dépend de deux abstractions injectées via le constructeur : IOrderRepository pour persister l’agrégat de domaine Order (modèle d’écriture) et IOrderReadModelStore pour maintenir le modèle de lecture synchronisé.
Dans la méthode asynchrone Handle, le handler commence par travailler sur le domaine (partie écriture) : il crée une nouvelle commande via Order.Create, ajoute des lignes (articles, quantités, prix), puis persiste l’entité avec _orders.Save(order). Ensuite, il construit un OrderReadModel à partir des données de l’agrégat (id, client, date de création, total) : c’est la projection utilisée pour les lectures. Enfin, il appelle _readModels.Upsert(readModel) pour insérer ou mettre à jour ce modèle de lecture, puis retourne order.Id. On profite ainsi d’un handler de commande qui met à jour l’écriture et le modèle de lecture, tout en exposant à l’appelant l’identifiant de la ressource créée.
#region Command + Handler (écriture)
public record CreateOrderCommand(Guid CustomerId) : ICommand;
// Handler de commande : applique la logique métier, sauvegarde, puis met à jour le modèle de lecture.
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _orders;
private readonly IOrderReadModelStore _readModels;
public CreateOrderCommandHandler(IOrderRepository orders, IOrderReadModelStore readModels)
{
_orders = orders;
_readModels = readModels;
}
public async Task<Guid> Handle(CreateOrderCommand command)
{
// 1. Domaine (écriture)
var order = Order.Create(command.CustomerId);
order.AddLine("Article A", 2, 10m);
order.AddLine("Article B", 1, 20m);
await _orders.Save(order);
// 2. Projection / modèle de lecture
var readModel = new OrderReadModel
{
Id = order.Id,
CustomerId = order.CustomerId,
CreatedAt = order.CreatedAt,
Total = order.GetTotal()
};
await _readModels.Upsert(readModel);
return order.Id;
}
}
#endregion
4. Bus CQRS simple
Cette classe CqrsBus joue le rôle de « bus » très simple pour CQRS : c’est un point central par lequel passent toutes les commandes (Send) et toutes les requêtes (Query). Elle ne contient aucune logique métier elle‑même ; son unique responsabilité est de trouver le bon handler et de lui déléguer le travail.
Le bus reçoit dans son constructeur une instance d’IServiceProvider, c’est‑à‑dire le conteneur d’injection de dépendances de .NET. Ce conteneur sait comment instancier les différents ICommandHandler<> et IQueryHandler<,> configurés dans l’application. En stockant cette dépendance dans _serviceProvider, le bus peut demander dynamiquement le handler approprié à chaque appel.
La méthode générique Send<TCommand> est utilisée pour exécuter une commande. Elle est contrainte par where TCommand : ICommand, ce qui garantit que seul un type marquant une commande peut être envoyé. À partir du type TCommand, le bus demande au conteneur un ICommandHandler<TCommand> via GetService. Le résultat est casté en ICommandHandler<TCommand> et le point d’exclamation ! indique au compilateur que l’on suppose que le service ne sera pas nul. Une fois le handler obtenu, le bus appelle simplement Handle(command) et renvoie la Task associée.
De manière symétrique, la méthode générique Query<TQuery, TResult> gère les lectures. Le paramètre TQuery est contraint à IQuery<TResult>, ce qui lie explicitement le type de requête au type de résultat retourné. Le bus récupère le handler correspondant IQueryHandler<TQuery, TResult> depuis l’IServiceProvider, puis appelle Handle(query) et renvoie la Task<TResult>. Ainsi, le code client n’a pas besoin de connaître quel handler concret est utilisé : il envoie une commande ou une requête au bus, qui se charge de router l’appel vers la bonne implémentation, ce qui favorise un couplage faible et une architecture modulaire.
#region "Bus" très simple (dispatch commands / queries)
public class CqrsBus
{
private readonly IServiceProvider _serviceProvider;
public CqrsBus(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task Send<TCommand>(TCommand command) where TCommand : ICommand
{
var handler = (ICommandHandler<TCommand>)_serviceProvider
.GetService(typeof(ICommandHandler<TCommand>))!;
return handler.Handle(command);
}
public Task<TResult> Send<TCommand, TResult>(TCommand command) where TCommand : ICommand
{
var handler = (ICommandHandler<TCommand, TResult>)_serviceProvider
.GetService(typeof(ICommandHandler<TCommand, TResult>))!;
return handler.Handle(command);
}
public Task<TResult> Query<TQuery, TResult>(TQuery query) where TQuery : IQuery<TResult>
{
var handler = (IQueryHandler<TQuery, TResult>)_serviceProvider
.GetService(typeof(IQueryHandler<TQuery, TResult>))!;
return handler.Handle(query);
}
}
#endregion
5. Modèle d’écriture et de lecture
Ce bloc de code fournit l’implémentation “infrastructure” de l’exemple CQRS :
La classe Order (et OrderLine) représente l’agrégat de domaine côté écriture, avec sa logique métier minimale (création, ajout de lignes, calcul du total) et un dépôt IOrderRepository + InMemoryOrderRepository pour le persister en mémoire. La classe OrderReadModel et IOrderReadModelStore / InMemoryOrderReadModelStore représentent le modèle de lecture, séparé et optimisé pour la consultation. La classe SimpleServiceProvider est un mini conteneur d’injection de dépendances utilisé pour enregistrer et résoudre les services (repositories, handlers, etc.) dans l’exemple.
#region Domaine (écriture)
public class Order
{
public Guid Id { get; private set; }
public Guid CustomerId { get; private set; }
public DateTime CreatedAt { get; private set; }
public List<OrderLine> Lines { get; private set; } = new();
private Order() { }
public static Order Create(Guid customerId)
{
return new Order
{
Id = Guid.NewGuid(),
CustomerId = customerId,
CreatedAt = DateTime.UtcNow
};
}
public void AddLine(string productName, int quantity, decimal unitPrice)
{
if (quantity <= 0) throw new ArgumentException("Quantity must be > 0");
Lines.Add(new OrderLine(productName, quantity, unitPrice));
}
public decimal GetTotal() => Lines.Sum(x => x.Quantity * x.UnitPrice);
}
public record OrderLine(string ProductName, int Quantity, decimal UnitPrice);
// Repository d’écriture (in-memory pour l’exemple)
public interface IOrderRepository
{
Task Save(Order order);
Task<Order?> GetById(Guid id);
}
public class InMemoryOrderRepository : IOrderRepository
{
private readonly Dictionary<Guid, Order> _storage = new();
public Task Save(Order order)
{
_storage[order.Id] = order;
return Task.CompletedTask;
}
public Task<Order?> GetById(Guid id)
{
_storage.TryGetValue(id, out var order);
return Task.FromResult(order);
}
}
#endregion
#region Modèle de lecture
public class OrderReadModel
{
public Guid Id { get; set; }
public Guid CustomerId { get; set; }
public DateTime CreatedAt { get; set; }
public decimal Total { get; set; }
}
// "Vue" de lecture en mémoire
public interface IOrderReadModelStore
{
Task Upsert(OrderReadModel model);
Task<OrderReadModel?> GetById(Guid id);
}
public class InMemoryOrderReadModelStore : IOrderReadModelStore
{
private readonly Dictionary<Guid, OrderReadModel> _storage = new();
public Task Upsert(OrderReadModel model)
{
_storage[model.Id] = model;
return Task.CompletedTask;
}
public Task<OrderReadModel?> GetById(Guid id)
{
_storage.TryGetValue(id, out var model);
return Task.FromResult(model);
}
}
#region Wiring simple (sans DI framework)
public class SimpleServiceProvider : IServiceProvider
{
private readonly Dictionary<Type, Func<object>> _factories = new();
public void RegisterSingleton<TService>(TService instance)
=> _factories[typeof(TService)] = () => instance!;
public void RegisterTransient<TService>(Func<TService> factory)
=> _factories[typeof(TService)] = () => factory!();
public object? GetService(Type serviceType)
=> _factories.TryGetValue(serviceType, out var f) ? f() : null;
}
#endregion
## 6. Programme principal
Ce code correspond au programme principal qui **démontre le flux CQRS de bout en bout** :
- Il instancie l’« infrastructure » (conteneur simple, dépôts en mémoire, handlers, bus CQRS).
- Il envoie une **commande** `CreateOrderCommand` via le bus pour créer une commande et mettre à jour le modèle de lecture.
- Il récupère l’ID de la commande créée, exécute ensuite une **requête** `GetOrderDetailsQuery` pour lire son détail depuis le modèle de lecture.
- Enfin, il affiche dans la console les informations de la commande (id, client, date, total).
```csharp
public class Program
{
public static async Task Main()
{
// 1. Wiring
var sp = new SimpleServiceProvider();
var orderRepo = new InMemoryOrderRepository();
var readStore = new InMemoryOrderReadModelStore();
sp.RegisterSingleton<IOrderRepository>(orderRepo);
sp.RegisterSingleton<IOrderReadModelStore>(readStore);
sp.RegisterTransient<ICommandHandler<CreateOrderCommand, Guid>>(() => new CreateOrderCommandHandler(orderRepo, readStore));
sp.RegisterTransient<IQueryHandler<GetOrderDetailsQuery, OrderReadModel?>>(() => new GetOrderDetailsQueryHandler(readStore));
var bus = new CqrsBus(sp);
// 2. Écriture : envoi d’une commande
var customerId = Guid.NewGuid();
var createCommand = new CreateOrderCommand(customerId);
var orderId = await bus.Send<CreateOrderCommand, Guid>(createCommand);
// 3. Lecture : exécution d’une requête
var query = new GetOrderDetailsQuery(orderId);
var readModel = await bus.Query<GetOrderDetailsQuery, OrderReadModel?>(query);
Console.WriteLine($"Order {readModel!.Id} for customer {readModel.CustomerId}");
Console.WriteLine($"Created at: {readModel.CreatedAt:u}");
Console.WriteLine($"Total: {readModel.Total} €");
}
}
7. Conclusion
Cet exemple minimal de CQRS en C# illustre concrètement la séparation entre écriture et lecture : les commandes manipulent l’agrégat de domaine Order et mettent à jour un modèle de lecture dédié (OrderReadModel), tandis que les requêtes s’appuient exclusivement sur ce modèle de lecture, optimisé pour la consultation. Même si l’implémentation reste volontairement simplifiée (stockage en mémoire, bus et conteneur maison), elle met en évidence les bénéfices essentiels du pattern : clarification des responsabilités, meilleure testabilité, possibilité d’adapter indépendamment les modèles et pipelines d’écriture et de lecture.
Dans un contexte réel, cette base pourrait être enrichie par une persistance durable (base de données), un véritable conteneur d’injection de dépendances, une couche d’événements pour alimenter plusieurs projections, ainsi que des mécanismes de mise en cache et de montée en charge. L’exemple fourni doit donc être vu comme un socle pédagogique permettant de comprendre la mécanique de CQRS avant de l’appliquer, de manière plus industrielle, dans des architectures plus complexes.
Points importants :
- Les commandes (
CreateOrderCommand) passent par un handler d’écriture qui manipule l’agrégatOrderet met ensuite à jour un modèle de lecture (OrderReadModel). - Les requêtes (
GetOrderDetailsQuery) ne lisent que le modèle de lecture, sans toucher à l’agrégat ni modifier l’état. - Le petit
CqrsBusillustre la séparation entreSend(commandes) etQuery(requêtes).
Consistance éventuelle en pratique
Dans une architecture CQRS, la consistance entre le modèle d’écriture et les modèles de lecture est souvent éventuelle : une commande validée ne se reflète pas instantanément dans toutes les projections de lecture. En pratique, cela implique plusieurs points à gérer explicitement.
Sur le plan fonctionnel, il faut accepter qu’un utilisateur puisse ne pas voir immédiatement le résultat d’une action (ex. une commande créée qui n’apparaît pas encore dans la liste). On compense par des choix d’UX : messages “opération en cours de traitement”, rafraîchissement automatique, boutons “actualiser”, ou affichage d’un état intermédiaire (“pending”). L’important est de ne pas promettre visuellement une synchronicité que le système ne garantit pas.
Sur le plan technique, la consistance éventuelle se gère par des pipelines de projections robustes : files de messages, mécanismes de retry, idempotence des handlers, métriques de délai entre écriture et mise à jour des vues. Il est également fréquent de distinguer des vues “critiques”, qui exigent une mise à jour plus stricte (voire une lecture directe sur la base d’écriture), de vues “non critiques”, pour lesquelles un léger décalage est acceptable (dashboards, reporting). CQRS oblige ainsi à rendre explicites les compromis entre fraîcheur de la donnée, performance et complexité.
Quand ne pas utiliser CQRS
CQRS apporte une structure puissante, mais aussi une complexité architecturale non négligeable (deux modèles, projections, éventuellement events, caches, etc.). Il n’est donc pas adapté à tous les contextes. Pour une petite application CRUD simple, avec un domaine peu complexe, un volume de données modéré et peu de besoins de vues spécialisées, CQRS risque d’introduire plus de coûts (code, opérations, compréhension) que de bénéfices.
De même, dans une équipe peu nombreuse ou peu expérimentée en architecture distribuée, la mise en place d’un véritable pipeline de projections, d’event sourcing, de cache distribué et de supervision peut être disproportionnée. Dans ces cas, un modèle “classique” (lecture/écriture dans le même modèle, voire le même service) reste souvent préférable, quitte à introduire CQRS plus tard sur des zones ciblées du système qui le justifient vraiment.
Enfin, CQRS n’est pas une solution magique à tous les problèmes de performance ou de conception. Si les difficultés viennent d’un schéma de données mal pensé, d’index manquants, d’un manque de cache ou d’un mauvais dimensionnement de l’infrastructure, l’introduction de CQRS ne résoudra pas ces problèmes par elle‑même. Il est donc important d’identifier clairement les motivations (complexité métier, besoins de nombreuses vues de lecture, forte dissymétrie lecture/écriture, besoin de scaling fin) avant d’adopter ce pattern.
Les bibliothèques CQRS en .NET
Il existe plusieurs bibliothèques et frameworks qui facilitent la mise en œuvre du pattern CQRS en .NET. Voici quelques-unes des options les plus populaires :
- MediatR : Une bibliothèque légère qui implémente le pattern Mediator, souvent utilisée pour gérer les commandes et les requêtes dans une architecture CQRS. Elle permet de décorréler les émetteurs de commandes/requêtes de leurs handlers. Du meme auteur qu’AutoMapper, ces deux bibliothèques sont desormais payantes pour les entreprises.
- EventFlow : Un framework CQRS et Event Sourcing pour .NET qui fournit des abstractions pour les agrégats, les commandes, les événements et les projections. -Medialor : Une alternative à MediatR, offrant des fonctionnalités similaires pour la gestion des commandes et des requêtes. Les interfaces sont un peu différentes, il est facile de migrer de l’une a l’autre. Medialor est open source et gratuit et utilise les sources générator pour éviter la reflection.