Dans l'ingénierie logicielle moderne, la pression pour équilibrer un débit de transactions élevé avec une conformité stricte aux audits est incessante. Les applications financières, les plateformes de santé et les systèmes de gestion d'inventaire font souvent face à une double exigence : ils doivent traiter des millions de transactions par seconde tout en maintenant un historique immuable et inviolable de chaque changement d'état. Les architectures CRUD (Create, Read, Update, Delete) traditionnelles peinent souvent à répondre simultanément à ces deux exigences, ce qui entraîne des goulots d'étranglement de performance et une logique de réconciliation complexe.
C'est ici que la combinaison du Command Query Responsibility Segregation (CQRS) et de l'Event Sourcing brille. Lorsqu'elle est correctement implémentée avec PostgreSQL, un moteur de base de données relationnel robuste, vous pouvez obtenir un système qui est non seulement performant sous une charge lourde, mais aussi intrinsèquement conforme aux normes réglementaires. Cet article explore les modèles architecturaux, les stratégies de conception de base de données et les détails d'implémentation pratiques pour construire de tels systèmes.
Comprendre les modèles fondamentaux
L'Event Sourcing change la manière fondamentale dont les données sont stockées. Au lieu de stocker l'état actuel d'une entité (par exemple, une Commande avec un statut « Expédiée »), vous stockez une séquence d'événements immuables qui ont conduit à cet état (par exemple, « CommandeCréée », « PaiementTraité », « ArticleExpédié »). Pour récupérer l'état actuel, vous rejouez ces événements. Cette approche fournit par définition une piste d'audit complète.
Le CQRS sépare le modèle en écriture du modèle en lecture. Dans l'Event Sourcing, les écritures sont coûteuses car elles impliquent l'ajout à un journal en lecture seule (append-only log) et potentiellement la mise à jour des projections. Les lectures, en revanche, doivent être optimisées pour les performances de requête. En découplant ces préoccupations, vous pouvez mettre à l'échelle vos réplicas en lecture indépendamment de vos serveurs en écriture, garantissant un débit élevé quelle que soit la complexité des requêtes.
Conception PostgreSQL pour les flux d'événements
PostgreSQL est un excellent choix pour l'Event Sourcing grâce à sa conformité ACID, à son puissant support JSONB et à son excellent contrôle de concurrence. La table principale d'un magasin d'événements est généralement un journal en lecture seule (append-only). Voici une définition de schéma qui privilégie les performances d'écriture et l'intégrité des données.
CREATE TABLE event_store (
id BIGSERIAL PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
version INTEGER NOT NULL,
occurred_at TIMESTAMPTZ DEFAULT NOW()
);
-- Les index sont critiques pour les performances
CREATE INDEX idx_event_aggregate ON event_store (aggregate_id, version);
CREATE INDEX idx_event_occurred ON event_store (occurred_at DESC);
Remarquez la colonne version. Elle est essentielle pour le contrôle de concurrence optimiste. Lors de l'écriture d'un événement, l'application vérifie si la version actuelle de l'agrégat correspond à la version attendue. Si c'est le cas, l'événement est ajouté et la version est incrémentée. Sinon, un conflit se produit et l'écriture échoue, empêchant les conditions de course.
Mise en œuvre du contrôle de concurrence optimiste
Un débit élevé nécessite des mécanismes de verrouillage efficaces. Au lieu de verrous au niveau des lignes qui peuvent sérialiser les écritures, l'Event Sourcing s'appuie sur la concurrence optimiste. Voici comment implémenter une écriture transactionnelle dans PostgreSQL en utilisant PL/pgSQL.
CREATE OR REPLACE FUNCTION append_event(
p_aggregate_id UUID,
p_event_type VARCHAR,
p_payload JSONB,
p_expected_version INTEGER
) RETURNS BIGINT AS $$
DECLARE
v_new_version INTEGER;
v_row_count INTEGER;
BEGIN
-- Vérifier si l'agrégat existe et valider la version
SELECT version INTO v_new_version
FROM event_store
WHERE aggregate_id = p_aggregate_id
ORDER BY version DESC
LIMIT 1;
IF v_new_version IS NULL THEN
v_new_version := 0;
END IF;
IF v_new_version != p_expected_version THEN
RAISE EXCEPTION 'Conflit de concurrence : version attendue % mais trouvée %',
p_expected_version, v_new_version;
END IF;
-- Insérer le nouvel événement
INSERT INTO event_store (aggregate_id, event_type, payload, version)
VALUES (p_aggregate_id, p_event_type, p_payload, v_new_version + 1);
RETURN v_new_version + 1;
END;
$$ LANGUAGE plpgsql;
Projections pour des lectures haute performance
Puisque la lecture à partir du flux d'événements brut est coûteuse en calcul, le CQRS dicte que nous maintenions des projections (ou vues matérialisées) séparées pour les opérations de lecture. Ces projections sont mises à jour de manière asynchrone ou synchrone au fur et à mesure que les événements sont validés. Par exemple, vous pourriez maintenir une table dénormalisée orders_summary qui permet des requêtes rapides par client ou par date, sans avoir besoin de rejouer l'intégralité de l'historique de chaque commande.
Pour garantir la conformité aux audits, le magasin d'événements reste la source de vérité. Les projections sont des données dérivées. Si une incohérence est détectée, vous pouvez reconstruire n'importe quelle projection à partir du flux d'événements immuable, garantissant ainsi une cohérence totale.
Conclusion
Implémenter l'Event Sourcing et le CQRS avec PostgreSQL n'est pas une solution miracle ; cela introduit une complexité en termes de logique applicative et de gestion de la cohérence éventuelle. Cependant, pour les systèmes nécessitant un débit élevé et des pistes d'audit strictes, il offre une base architecturale supérieure. En tirant parti des fonctionnalités robustes de PostgreSQL pour le contrôle de concurrence et le stockage JSONB, les développeurs peuvent construire des systèmes qui sont à la fois évolutifs et conformes. La clé est de concevoir soigneusement vos projections et d'investir dans des gestionnaires d'événements robustes pour maintenir le côté lecture synchronisé avec l'historique immuable stocké du côté écriture.