11 - Modèle mental et vocabulaire event-driven

La plupart des échecs en event-driven ne viennent pas de Kafka ou des brokers, mais d’un modèle mental flou. Quand les équipes utilisent le même mot pour parler de choses différentes, les architectures deviennent incohérentes. Cet article propose un modèle mental orienté flux, puis un vocabulaire rigoureux adapté au contexte bancaire.

Le modèle mental: intention → décision → event → projection → query

Un système event-driven sain suit une séquence logique. En banque, cette séquence garantit que les invariants sont respectés avant que l’information ne se diffuse.

1) Intention (command): un client demande un virement. C’est une intention, pas un fait. Elle peut être refusée. 2) Décision: un command handler vérifie les invariants (solde, KYC, limites, statut du compte). La décision est locale et atomique. 3) Event (fait): après validation et persistance, le service publie TransferInitiated. C’est un fait immuable. 4) Projection: des consumers construisent leurs propres vues (ledger, fraude, notifications, reporting). Chaque projection est indépendante. 5) Query: l’API lit un read model pour afficher un état au client.

Le point clé: les projections ne dictent pas la décision. Elles la reflètent avec un délai éventuel. Cela évite que des vues secondaires imposent des règles au cœur métier.

Invariants bancaires: le cœur du modèle mental

Les invariants sont les règles non négociables qui structurent la décision. Dans une banque, ils sont nombreux et stricts: - Un compte ne doit pas passer sous un certain seuil sans autorisation. - Un virement ne doit pas contourner les règles AML/KYC. - Les écritures comptables doivent être en double-entry.

Ces invariants vivent côté write model. C’est pourquoi les commands sont synchrones et strictement contrôlées. Les events sont un résultat de cette validation, jamais un substitut. Si un consumer tente de “valider” un virement après coup, le système devient incohérent.

Command handling: frontière de responsabilité

Un command handler ne se contente pas de “faire” une action. Il décide si l’action est possible. Dans une banque, cela implique plusieurs couches: - Authentification/autorisation: le client est-il habilité ? - Validation métier: solde, plafonds, règles KYC/AML. - Idempotence: la commande est-elle un doublon ? - Persistance atomique: la décision et l’event doivent être cohérents.

Cette frontière est essentielle pour éviter les effets de bord. Un consumer ne doit pas réinterpréter la décision. Il peut enrichir, notifier, ou projeter, mais pas redéfinir l’invariant. Une bonne pratique consiste à distinguer les refus métiers (TransferRejected) des erreurs techniques (TransferFailed), afin de clarifier ce qui relève du domaine et ce qui relève de l’infrastructure.

Vocabulaire structurant

Command, event, query

  • Command: intention impérative, handler unique, peut échouer.
  • Event: fait immuable, publié après la décision, fan-out naturel.
  • Query: lecture sans effet de bord, appuyée sur une projection.

Aggregate, invariant, transaction

  • Aggregate: frontière d’atomicité du write model (ex: compte bancaire).
  • Invariant: règle métier qui doit toujours être vraie.
  • Transaction: garantit l’atomicité d’une décision locale.

Projection, read model, materialized view

  • Projection: traitement qui transforme des events en vue utilisable.
  • Read model: base optimisée pour la lecture (mobile, backoffice, BI).
  • Materialized view: projection persistée, souvent dans une base dédiée.

Event store, log, offset

  • Event store / log: stockage append-only des events.
  • Offset: position de lecture d’un consumer dans un topic.

Event envelope

Un event contient un envelope (metadata) et un payload (métier). Metadata typiques: - event_id, occurred_at, schema_version - correlation_id, causation_id

Le correlation_id relie les events d’un même flux, le causation_id relie un fait à l’intention qui l’a déclenché.

Nommage et intention sémantique

Le nom d’un event est un contrat mental. Il doit être au passé, explicite, et orienté métier. TransferInitiated, TransferSettled, AccountFrozen sont des faits. À l’inverse, TransferUpdated ou AccountChanged sont ambigus et forcent les consumers à deviner la signification exacte.

Dans un domaine bancaire, le nom doit refléter l’impact comptable ou opérationnel. Exemple: TransferSettled implique que les écritures double-entry ont été passées, alors que TransferAuthorized ne signifie qu’une validation de règles. Ce niveau de précision réduit les malentendus entre équipes.

Temps, ordre et causalité

L’event-driven impose une pensée temporelle. Trois notions sont essentielles:

  • Event time: moment réel du fait (occurred_at).
  • Processing time: moment où le consumer traite l’event.
  • Ordering: ordre garanti uniquement par partition.

Dans une banque, ces distinctions sont critiques. Exemple: un virement initié à 10:00 peut être traité à 10:05 si le consumer est en retard. Le reporting réglementaire doit utiliser le temps d’événement, pas le temps de traitement.

La causalité est suivie via correlation_id et causation_id. Un flux de virement peut produire plusieurs events (TransferInitiated, TransferSettled, TransferNotified), tous corrélés. Cette chaîne est la base de l’audit.

Partitionnement et clé métier

Le partitionnement est un concept technique, mais c’est surtout une décision métier. L’ordre n’est garanti qu’à l’intérieur d’une partition. Choisir la clé de partition revient donc à choisir quelle entité doit être ordonnée.

Dans une banque, l’ordre des opérations sur un compte est critique. Une clé account_id garantit que les débits/crédits d’un même compte sont traités dans le bon ordre. En revanche, une clé customer_id peut mélanger plusieurs comptes et violer l’ordering attendu. Pour un flux de virement, la clé transfer_id peut suffire si l’ordre par transfert est la priorité, mais elle ne garantit pas l’ordre des opérations de compte.

Il faut aussi gérer la distribution: une clé trop concentrée crée un hot partition qui limite la scalabilité. Les banques arbitrent souvent entre cohérence (ordre strict) et performance (répartition uniforme). L’important est de documenter ce choix, car il conditionne la validité des projections.

Idempotence et sémantique de livraison

Kafka et la majorité des brokers garantissent at-least-once par défaut. Cela signifie que les doublons sont possibles. Dans un domaine bancaire, un consumer non idempotent est un risque majeur (double écriture comptable).

Stratégies classiques: - Déduplication par event_id dans une table dédiée. - Upsert sur une projection par clé métier. - Idempotent producer pour éviter les doublons côté publication.

Les commands doivent être idempotentes, via command_id. Les events doivent être traités comme potentiellement redondants: chaque handler doit être capable à la fois d’appliquer et d’ignorer un doublon.

Schémas, versioning et contrat d’événements

Un event public est un contrat. Sa structure ne peut pas changer arbitrairement. Les pratiques robustes incluent: - Versionner les schémas (Avro/Protobuf/JSON Schema). - Favoriser les changements additifs (nouveaux champs optionnels). - Éviter les renommages/suppressions sans version majeure. - Documenter la compatibilité (backward/forward/full).

Dans une banque, un event TransferSettled consommé par la comptabilité et le reporting ne peut pas changer sans coordination. Les équipes mettent souvent en place des contract tests producer/consumer pour détecter les ruptures avant déploiement.

Cohérence, latence et read-your-writes

Un read model est souvent éventuellement cohérent. Cela implique que la lecture peut être légèrement en retard par rapport à la décision. En banque, ce délai est acceptable si l’UX est explicite: un virement est “en cours” avant d’être “settled”.

Pour limiter la frustration utilisateur, on met en place des stratégies read-your-writes: - renvoyer un token de cohérence après la command, - fallback sur le write model si la projection n’est pas à jour, - cache session pour afficher immédiatement la décision.

L’objectif est d’offrir une UX rapide sans compromettre la robustesse.

Replay, reprocessing et déterminisme

Rejouer un flux permet de reconstruire un état, mais impose un traitement déterministe. Les effets externes (notifications, scoring externe, appels FX) doivent être isolés. Deux approches classiques: - Gateway désactivable en mode replay. - Journalisation des requêtes externes pour rejouer les mêmes valeurs.

Dans une banque, le replay est utilisé pour reconstruire un ledger après incident ou recalculer des indicateurs. Sans discipline, le replay devient impraticable et dangereux.

Orchestration vs chorégraphie (sagas)

Quand un processus traverse plusieurs domaines, on parle de saga. Deux approches coexistent: - Chorégraphie: chaque service réagit aux events et publie le suivant. - Orchestration: un orchestrateur central pilote la séquence.

En banque, la chorégraphie favorise le découplage, mais rend le flux moins visible. L’orchestration apporte de la lisibilité et des timeouts centralisés, mais introduit un point de contrôle unique. Les deux approches doivent prévoir des compensations et des délais (ex: annuler un virement si le scoring fraude dépasse un seuil après un certain temps).

Compensation vs reversal: ne pas confondre

Une compensation annule un effet métier par un nouveau flux, souvent dans le cadre d’une saga. Une reversal est une correction comptable formelle.

Exemple bancaire: - Compensation: annuler un virement en lançant un flux inverse. - Reversal: enregistrer une écriture corrective avec TransferReversed.

L’event-driven privilégie la correction explicite plutôt que la suppression ou la modification d’un event. L’historique doit rester immuable pour l’audit.

Événements métiers vs techniques

  • Événements métiers: faits business (ex: AccountOpened, TransferSettled).
  • Événements techniques: signaux d’infrastructure (ex: ConsumerLagHigh).

Il faut séparer ces flux, tant pour la gouvernance que pour la lisibilité des systèmes. Les events métiers sont des contrats durables; les events techniques sont des signaux d’exploitation à rétention courte.

Public vs private events

Les events publics sont des contrats d’intégration. Ils doivent être stables, versionnés, documentés. Les events private peuvent évoluer plus vite, car ils sont internes à une équipe.

Un TransferSettled public doit être conçu comme un contrat. Un event interne transfer_state_cache_updated n’a pas vocation à sortir d’un domaine.

Qualité de service et observabilité

Le vocabulaire doit intégrer la notion de SLA. Un event “perdu” ou traité avec dix minutes de retard peut être acceptable pour un reporting, mais pas pour un virement instantané. Les métriques clés incluent le lag, le taux d’erreur, et la latence end-to-end. Les banques instrumentent les flux via tracing distribué et alertes sur les consumer groups.

Sans cette observabilité, l’architecture event-driven devient une boîte noire.

Anti-patterns sémantiques

  • Event comme commande déguisée: on publie un event en espérant une action obligatoire d’un consumer. Cela cache un couplage fort.
  • Noms ambigus: TransferUpdated ne dit rien du fait réel.
  • Deltas publics: exposer un delta au lieu d’un fait complet casse les consumers.
  • Projections qui dictent la décision: la lecture ne doit jamais contrôler l’écriture.

Checklist mentale

Avant de publier un event, se poser ces questions: - Est-ce un fait ou une intention ? - Qui est le propriétaire du contrat ? - Quelle est la clé de partition et pourquoi ? - Le consumer est-il idempotent ? - L’event est-il rejouable sans effets externes ?

Conclusion

Le vocabulaire event-driven est plus qu’un dictionnaire: c’est une discipline. En banque, où l’audit et la conformité sont critiques, un modèle mental rigoureux est la seule manière d’éviter les incohérences. La maîtrise du flux intention-décision-event-projection est la base sur laquelle on construit des architectures résilientes et évolutives. Ce cadre commun simplifie la communication entre équipes et réduit les dérives architecturales sur le long terme.