CQRS

CQRS

Command Query Responsibility Segregation

Un pattern d'architecture qui sépare les opérations de lecture et d'écriture pour optimiser la performance, la scalabilité et la maintenance.

Pour les non-initiés

Qu'est-ce que CQRS ?

Imaginez une bibliothèque municipale très fréquentée. Pour gérer efficacement le flux constant de visiteurs, la bibliothèque pourrait décider de créer deux comptoirs distincts :

  • Un comptoir pour emprunter et retourner des livres (modifications)
  • Un autre comptoir pour rechercher et consulter le catalogue (lectures)

C'est exactement ce que fait CQRS (Command Query Responsibility Segregation) dans le monde du développement logiciel : il sépare les opérations qui modifient les données (commandes) de celles qui lisent les données (requêtes).

Pourquoi adopter CQRS ?

Performance

Chaque côté peut être optimisé indépendamment pour ce qu'il fait le mieux : les lectures peuvent être rapides et les écritures peuvent être sécurisées.

Scalabilité

Les systèmes de lecture et d'écriture peuvent évoluer indépendamment selon les besoins spécifiques de l'application.

En résumé, CQRS permet à des systèmes complexes de rester performants et maintenables en séparant clairement deux préoccupations fondamentales : la modification des données et leur consultation.

Pour les développeurs

Fonctionnement technique

CQRS (Command Query Responsibility Segregation) est un pattern architectural qui sépare les opérations de modification (commandes) des opérations de lecture (requêtes). Cette séparation permet d'optimiser chaque côté pour ses besoins spécifiques.

Les principes fondamentaux

Commandes vs Requêtes

  • Commandes : Modifient l'état du système, ne retournent pas de données, peuvent être mises en file d'attente
  • Requêtes : Récupèrent des données, ne modifient pas l'état, sont généralement synchrones

Cette séparation stricte permet une meilleure maintenabilité et une évolution plus simple du code.

Implémentation des Commandes

Les commandes sont implémentées en Icône PHPPHP comme des objets simples contenant les données nécessaires à une action.

Command.php
<?php namespace App\CQRS\Command; class CreateProductCommand { public function __construct( public readonly string $name, public readonly string $description, public readonly float $price, public readonly ?string $sku = null ) { } }

Les commandes sont traitées par des handlers dédiés :

CommandHandler.php
<?php namespace App\CQRS\CommandHandler; use App\CQRS\Command\CreateProductCommand; use App\Entity\Product; use App\Repository\ProductWriteRepository; use Symfony\Component\Uid\Uuid; class CreateProductCommandHandler { public function __construct( private ProductWriteRepository $productRepository ) { } public function __invoke(CreateProductCommand $command): string { $productId = Uuid::v4()->toRfc4122(); $product = new Product( $productId, $command->name, $command->description, $command->price, $command->sku ); $this->productRepository->save($product); return $productId; } }

Implémentation des Requêtes

Les requêtes suivent un modèle similaire, mais sont optimisées pour la lecture :

Query.php
<?php namespace App\CQRS\Query; class GetProductQuery { public function __construct( public readonly string $productId ) { } }

Les requêtes sont traitées par leurs propres handlers :

QueryHandler.php
<?php namespace App\CQRS\QueryHandler; use App\CQRS\Query\GetProductQuery; use App\DTO\ProductDTO; use App\Repository\ProductReadRepository; class GetProductQueryHandler { public function __construct( private ProductReadRepository $productRepository ) { } public function __invoke(GetProductQuery $query): ?ProductDTO { $product = $this->productRepository->findById($query->productId); if (!$product) { return null; } return new ProductDTO( $product->getId(), $product->getName(), $product->getDescription(), $product->getPrice(), $product->getSku() ); } }

Bus de Commandes et de Requêtes

Pour dispatcher les commandes et les requêtes vers leurs handlers respectifs, on utilise généralement des bus :

CommandBus.php
<?php namespace App\CQRS\CommandBus; class CommandBus { private array $handlers = []; public function registerHandler(string $commandClass, callable $handler): void { $this->handlers[$commandClass] = $handler; } public function dispatch(object $command): mixed { $commandClass = get_class($command); if (!isset($this->handlers[$commandClass])) { throw new RuntimeException("No handler registered for command {$commandClass}"); } return ($this->handlers[$commandClass])($command); } }

Séparation des Modèles de Lecture et d'Écriture

CQRS permet également de séparer les modèles de données pour l'écriture et la lecture, ce qui est particulièrement utile pour les applications complexes :

Repositories.php
<?php // src/Repository/ProductReadRepository.php namespace App\Repository; use App\Entity\Product; use Doctrine\DBAL\Connection; class ProductReadRepository { public function __construct( private Connection $connection ) { } public function findById(string $id): ?array { $result = $this->connection->createQueryBuilder() ->select('p.*') ->from('products', 'p') ->where('p.id = :id') ->setParameter('id', $id) ->executeQuery() ->fetchAssociative(); return $result ?: null; } public function findByFilters(array $filters): array { $qb = $this->connection->createQueryBuilder() ->select('p.*') ->from('products', 'p'); // Ajouter les filtres... return $qb->executeQuery()->fetchAllAssociative(); } } // src/Repository/ProductWriteRepository.php namespace App\Repository; use App\Entity\Product; use Doctrine\ORM\EntityManagerInterface; class ProductWriteRepository { public function __construct( private EntityManagerInterface $entityManager ) { } public function save(Product $product): void { $this->entityManager->persist($product); $this->entityManager->flush(); } public function delete(Product $product): void { $this->entityManager->remove($product); $this->entityManager->flush(); } }

Intégration avec un Contrôleur Icône SymfonySymfony

ProductController.php
<?php namespace App\Controller; use App\CQRS\Command\CreateProductCommand; use App\CQRS\CommandBus\CommandBus; use App\CQRS\Query\GetProductQuery; use App\CQRS\QueryBus\QueryBus; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class ProductController extends AbstractController { public function __construct( private CommandBus $commandBus, private QueryBus $queryBus ) { } /** * @Route("/products", name="create_product", methods={"POST"}) */ public function create(Request $request): Response { $data = json_decode($request->getContent(), true); $command = new CreateProductCommand( $data['name'], $data['description'], (float) $data['price'], $data['sku'] ?? null ); $productId = $this->commandBus->dispatch($command); return $this->json(['id' => $productId], Response::HTTP_CREATED); } /** * @Route("/products/{id}", name="get_product", methods={"GET"}) */ public function getProduct(string $id): Response { $query = new GetProductQuery($id); $product = $this->queryBus->dispatch($query); if (!$product) { return $this->json(['error' => 'Product not found'], Response::HTTP_NOT_FOUND); } return $this->json($product); } }

Avantages techniques

  • Séparation des préoccupations - Chaque modèle peut évoluer indépendamment
  • Optimisation spécifique - Adaptations différentes pour lecture (souvent DBAL direct, vues, etc.) et écriture (souvent ORM)
  • Parallélisation - Possibilité de répartir la charge sur différents serveurs
  • Testabilité - Les commandes et les requêtes sont facilement testables isolément
  • Évolutivité - Ajout facile de nouvelles commandes ou requêtes sans modifier le code existant

Quand utiliser CQRS

  • Applications complexes avec modèles de domaine riches
  • Systèmes avec charge asymétrique (beaucoup plus de lectures que d'écritures)
  • Projets nécessitant des vues spécialisées des données
  • Systèmes qui bénéficieraient de la séparation des responsabilités
  • Applications utilisant Event Sourcing (une combinaison puissante)
Applications concrètes

Cas d'usage

E-commerce

Parfait pour gérer les catalogues de produits (lecture intensive) tout en traitant les commandes et la gestion des stocks (écriture) de manière séparée.

Finance et Banque

Pour séparer les transactions financières (traitement rigoureux des écritures) des rapports et vues de compte (lectures à hautes performances).

CRM et Gestion de relation client

Pour gérer efficacement les mises à jour des données clients tout en permettant des recherches et analyses rapides sur de grandes quantités de données.

Applications à haute charge

Idéal pour les applications avec des millions d'utilisateurs où les lectures sont beaucoup plus fréquentes que les écritures, permettant d'optimiser et de mettre à l'échelle chaque côté indépendamment.

Combinaison avec d'autres patterns

CQRS est souvent utilisé en combinaison avec d'autres patterns d'architecture pour créer des systèmes robustes et évolutifs :

Domain-Driven Design
Microservices
Hexagonal Architecture
Eventual Consistency
Message Queues