Sourcing
Event Sourcing
Un pattern architectural qui stocke les changements d'état sous forme d'événements plutôt que juste l'état final, permettant de reconstituer l'historique complet d'un système.
Qu'est-ce que l'Event Sourcing ?
Imaginez que vous utilisiez uniquement un relevé bancaire mensuel pour gérer vos finances. Vous n'auriez qu'une vision ponctuelle de votre solde, sans comprendre comment vous en êtes arrivé là.
Maintenant, imaginez que vous conserviez chaque reçu et chaque facture. Vous pourriez reconstituer votre situation financière à n'importe quel moment, comprendre pourquoi votre solde a évolué, et même corriger des erreurs en revenant à l'origine.
L'Event Sourcing, c'est exactement ça pour les applications : au lieu de stocker uniquement l'état actuel des données, on enregistre chaque changement significatif sous forme d'événements. Ces événements deviennent la source de vérité du système.
Pourquoi utiliser l'Event Sourcing ?
Historique complet
Comprendre exactement comment le système a évolué dans le temps et reconstruire l'état à n'importe quel moment.
Audit et conformité
Traçabilité parfaite pour les secteurs réglementés ou les opérations financières qui nécessitent une piste d'audit.
En résumé, l'Event Sourcing est particulièrement précieux dans les contextes où la compréhension de l'évolution d'un système dans le temps est aussi importante que son état actuel.
Fonctionnement technique
L'Event Sourcing est un pattern architectural où l'état d'une application est déterminé par une séquence d'événements plutôt que par des snapshots instantanés. Chaque changement d'état est représenté par un événement immutable qui est enregistré dans l'ordre chronologique.
Les composants clés
Événements de Domaine
Les événements sont des objets immutables qui représentent un fait qui s'est produit dans le système. Ils sont nommés au passé car ils représentent quelque chose qui est déjà arrivé.
<?php
namespace App\EventSourcing\Event;
use DateTimeImmutable;
abstract class DomainEvent
{
private string $eventId;
private DateTimeImmutable $occurredOn;
public function __construct(
private string $aggregateId
) {
$this->eventId = uniqid('ev_');
$this->occurredOn = new DateTimeImmutable();
}
public function eventId(): string
{
return $this->eventId;
}
public function aggregateId(): string
{
return $this->aggregateId;
}
public function occurredOn(): DateTimeImmutable
{
return $this->occurredOn;
}
abstract public function eventName(): string;
abstract public function toPrimitives(): array;
}
Un exemple concret d'événement :
<?php
namespace App\EventSourcing\Event;
class ProductCreatedEvent extends DomainEvent
{
public function __construct(
string $productId,
private string $name,
private string $description,
private float $price,
private ?string $sku = null
) {
parent::__construct($productId);
}
public function eventName(): string
{
return 'product.created';
}
public function toPrimitives(): array
{
return [
'name' => $this->name,
'description' => $this->description,
'price' => $this->price,
'sku' => $this->sku,
];
}
public function name(): string
{
return $this->name;
}
public function description(): string
{
return $this->description;
}
public function price(): float
{
return $this->price;
}
public function sku(): ?string
{
return $this->sku;
}
}
Agrégats (Aggregates)
Les agrégats sont des entités qui appliquent les événements pour mettre à jour leur état interne. En Event Sourcing, ils produisent également de nouveaux événements lorsque leur état change.
<?php
namespace App\EventSourcing\Aggregate;
use App\EventSourcing\Event\DomainEvent;
abstract class AggregateRoot
{
private array $recordedEvents = [];
/**
* @return DomainEvent[]
*/
public function pullDomainEvents(): array
{
$events = $this->recordedEvents;
$this->recordedEvents = [];
return $events;
}
protected function record(DomainEvent $event): void
{
$this->recordedEvents[] = $event;
$this->apply($event);
}
abstract protected function apply(DomainEvent $event): void;
}
Un exemple d'agrégat pour un produit :
<?php
namespace App\EventSourcing\Aggregate;
use App\EventSourcing\Event\DomainEvent;
use App\EventSourcing\Event\ProductCreatedEvent;
use App\EventSourcing\Event\ProductPriceChangedEvent;
use App\EventSourcing\Exception\ProductNotFoundException;
class Product extends AggregateRoot
{
private string $id;
private string $name;
private string $description;
private float $price;
private ?string $sku;
// Pour la reconstruction
public static function create(
string $id,
string $name,
string $description,
float $price,
?string $sku = null
): self {
$product = new self();
$product->record(new ProductCreatedEvent(
$id, $name, $description, $price, $sku
));
return $product;
}
// Pour le changement d'état
public function changePrice(float $newPrice): void
{
if ($this->price === $newPrice) {
return;
}
$this->record(new ProductPriceChangedEvent(
$this->id, $newPrice, $this->price
));
}
// Application des événements
protected function apply(DomainEvent $event): void
{
if ($event instanceof ProductCreatedEvent) {
$this->id = $event->aggregateId();
$this->name = $event->name();
$this->description = $event->description();
$this->price = $event->price();
$this->sku = $event->sku();
}
if ($event instanceof ProductPriceChangedEvent) {
$this->price = $event->newPrice();
}
}
// Reconstruction depuis l'historique des événements
public static function fromEvents(string $id, array $events): self
{
$product = new self();
foreach ($events as $event) {
$product->apply($event);
}
// Vérification que le produit a bien été initialisé
if (!isset($product->id)) {
throw new ProductNotFoundException($id);
}
return $product;
}
// Getters
public function id(): string
{
return $this->id;
}
public function name(): string
{
return $this->name;
}
public function description(): string
{
return $this->description;
}
public function price(): float
{
return $this->price;
}
public function sku(): ?string
{
return $this->sku;
}
}
Event Store
L'Event Store est le composant qui stocke tous les événements de manière persistante. Habituellement implémenté avec une base de données, il est responsable de l'enregistrement chronologique des événements.
<?php
namespace App\EventSourcing\Infrastructure;
use App\EventSourcing\Event\DomainEvent;
use App\EventSourcing\Repository\EventStoreInterface;
use Doctrine\DBAL\Connection;
class DoctrineEventStore implements EventStoreInterface
{
public function __construct(
private Connection $connection
) {
}
public function append(DomainEvent $event): void
{
$serialized = json_encode($event->toPrimitives());
$this->connection->executeStatement(
'INSERT INTO events (event_id, aggregate_id, event_name, body, occurred_on)
VALUES (:eventId, :aggregateId, :eventName, :body, :occurredOn)',
[
'eventId' => $event->eventId(),
'aggregateId' => $event->aggregateId(),
'eventName' => $event->eventName(),
'body' => $serialized,
'occurredOn' => $event->occurredOn()->format('Y-m-d H:i:s')
]
);
}
public function getEventsForAggregate(string $aggregateId): array
{
$rows = $this->connection->createQueryBuilder()
->select('*')
->from('events')
->where('aggregate_id = :aggregateId')
->setParameter('aggregateId', $aggregateId)
->orderBy('occurred_on', 'ASC')
->executeQuery()
->fetchAllAssociative();
// Reconstruire les événements à partir des données brutes
// (simplifié pour l'exemple)
$events = [];
foreach ($rows as $row) {
$events[] = $this->reconstructEvent($row);
}
return $events;
}
private function reconstructEvent(array $row): DomainEvent
{
// Logique de reconstruction des événements
// En pratique, on utiliserait une factory ou un système de réflexion
// ...
}
}
Projections
Les projections transforment les événements en vues optimisées pour les requêtes. Elles résolvent le problème de performance de lecture en construisant des représentations dédiées.
<?php
namespace App\EventSourcing\Projection;
use App\EventSourcing\Event\ProductCreatedEvent;
use App\EventSourcing\Event\ProductPriceChangedEvent;
use Doctrine\DBAL\Connection;
class ProductCatalogProjection
{
public function __construct(
private Connection $connection
) {
}
public function projectProductCreated(ProductCreatedEvent $event): void
{
$this->connection->executeStatement(
'INSERT INTO product_catalog (id, name, description, price, sku, created_at)
VALUES (:id, :name, :description, :price, :sku, :createdAt)',
[
'id' => $event->aggregateId(),
'name' => $event->name(),
'description' => $event->description(),
'price' => $event->price(),
'sku' => $event->sku(),
'createdAt' => $event->occurredOn()->format('Y-m-d H:i:s')
]
);
}
public function projectProductPriceChanged(ProductPriceChangedEvent $event): void
{
$this->connection->executeStatement(
'UPDATE product_catalog SET price = :price, updated_at = :updatedAt WHERE id = :id',
[
'id' => $event->aggregateId(),
'price' => $event->newPrice(),
'updatedAt' => $event->occurredOn()->format('Y-m-d H:i:s')
]
);
}
}
Intégration avec
Symfony
Exemple d'intégration dans une application Symfony :
<?php
namespace App\Controller;
use App\EventSourcing\Aggregate\Product;
use App\EventSourcing\Command\CreateProductCommand;
use App\EventSourcing\Command\ChangeProductPriceCommand;
use App\EventSourcing\Repository\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Uid\Uuid;
class ProductController extends AbstractController
{
public function __construct(
private MessageBusInterface $commandBus,
private ProductRepository $productRepository
) {
}
/**
* @Route("/products", name="create_product", methods={"POST"})
*/
public function create(Request $request): Response
{
$data = json_decode($request->getContent(), true);
$productId = Uuid::v4()->toRfc4122();
$command = new CreateProductCommand(
$productId,
$data['name'],
$data['description'],
(float) $data['price'],
$data['sku'] ?? null
);
$this->commandBus->dispatch($command);
return $this->json(['id' => $productId], Response::HTTP_CREATED);
}
/**
* @Route("/products/{id}/price", name="change_product_price", methods={"PUT"})
*/
public function changePrice(string $id, Request $request): Response
{
$data = json_decode($request->getContent(), true);
$command = new ChangeProductPriceCommand(
$id,
(float) $data['price']
);
$this->commandBus->dispatch($command);
return $this->json(null, Response::HTTP_NO_CONTENT);
}
/**
* @Route("/products/{id}/history", name="product_history", methods={"GET"})
*/
public function history(string $id): Response
{
$events = $this->productRepository->getProductEvents($id);
$history = array_map(function ($event) {
return [
'event' => $event->eventName(),
'data' => $event->toPrimitives(),
'occurred_on' => $event->occurredOn()->format('c')
];
}, $events);
return $this->json(['history' => $history]);
}
}
Avantages techniques
- Audit complet - Toutes les modifications sont préservées dans l'historique
- Debugging temporel - Possibilité de reconstruire l'état du système à n'importe quel point dans le temps
- Évolutivité - Séparation des responsabilités de lecture et d'écriture, souvent utilisé avec CQRS
- Intégration - Les événements peuvent alimenter directement d'autres systèmes
- Tests - Facilité à tester les comportements basés sur des séquences d'événements
Défis et considérations
- Courbe d'apprentissage - Paradigme différent des applications CRUD traditionnelles
- Évolution des événements - Gestion des versions des événements au fil du temps
- Performance des projections - Reconstruire des projections peut être coûteux
- Gestion de l'espace disque - Stockage de l'historique complet nécessite plus d'espace
- Cohérence - Assurer la cohérence entre l'event store et les projections
Cas d'usage
Systèmes financiers
Idéal pour les systèmes bancaires et comptables où chaque transaction doit être tracée et où l'historique complet est essentiel pour l'audit et la conformité.
Suivi de processus métier
Pour les workflows complexes comme la gestion de commandes, de réclamations ou de dossiers médicaux, où l'historique complet des décisions et des actions est crucial.
Systèmes collaboratifs
Pour les outils de chat, de gestion de contenu collaboratif, ou les applications d'édition partagée, où la séquence des modifications est importante.
Gestion des risques
Pour les systèmes où la reconstruction de séquences d'événements est essentielle pour l'analyse de défaillances, les systèmes de sécurité ou les plateformes de détection de fraudes.
Intégration avec d'autres patterns
L'Event Sourcing est souvent utilisé en combinaison avec d'autres patterns architecturaux :