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.
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.
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 PHP comme des objets simples contenant les données nécessaires à une action.
<?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 :
<?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 :
<?php
namespace App\CQRS\Query;
class GetProductQuery
{
public function __construct(
public readonly string $productId
) {
}
}
Les requêtes sont traitées par leurs propres handlers :
<?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 :
<?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 :
<?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
Symfony
<?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)
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 :