Event-driven avec Kafka : ce qu'on gagne, et ce qu'on perd

Sur MissionMatch, le streaming d'événements Kafka découple la publication d'une mission de tous les traitements qui doivent en découler : notification, indexation, statistiques. Le bénéfice de découplage est réel. Ce qu'on perd en échange l'est tout autant, et c'est rarement mis en avant avec la même clarté.
Le problème que Kafka résout vraiment
Sans event streaming, chaque nouveau traitement déclenché par un événement métier (une mission publiée, par exemple) demande de modifier le service qui publie cet événement, pour y ajouter un appel direct. Au bout de quelques traitements, ce service connaît tous ses consommateurs, ce qui va exactement à l'encontre du principe de responsabilité unique. Publier un événement sur un topic, et laisser chaque consommateur s'abonner indépendamment, inverse cette dépendance : le service producteur ignore qui consomme, et combien.
topic: mission.published
┌────────────────────────────────┐
Mission Service ──▶ │ ████ ████ ████ ████ ████ │
(producteur) └────────────────────────────────┘
│ │ │
▼ ▼ ▼
Notification Indexation Statistiques
(consumer) (consumer) (consumer)
Chaque consommateur avance à son propre rythme, se rejoue en cas d'échec indépendamment des autres, et peut être ajouté ou retiré sans toucher au service producteur ni aux autres consommateurs.
Ce que ce découplage fait perdre
Le prix de cette flexibilité, c'est la perte de garanties qu'on tenait pour acquises avec un appel synchrone direct. Un appel de fonction classique échoue de façon visible et immédiate. Un message Kafka peut être traité en retard, être dupliqué (au moins une fois, pas exactement une fois, sauf configuration spécifique), ou être traité dans le désordre selon le partitionnement choisi. Ces propriétés ne sont pas des bugs : elles font partie du contrat du système, mais elles doivent être conçues explicitement, pas découvertes en production.
Ce qui change concrètement, propriété par propriété
| Propriété | Appel synchrone direct | Événement Kafka |
|---|---|---|
| Livraison | Garantie ou exception immédiate | Au moins une fois (le consommateur doit gérer les doublons) |
| Ordre | Ordre d'exécution du code appelant | Garanti par partition, pas globalement |
| Latence perçue | Immédiate, bloquante | Asynchrone, le producteur ne connaît pas le résultat du traitement |
| Couplage au démarrage | Le service appelé doit être disponible | Le consommateur peut être arrêté, il rattrape à son retour |
Chaque ligne de ce tableau est un compromis explicite, pas un défaut caché du système : accepter « au moins une fois » plutôt que « exactement une fois » est un choix de conception qui déplace la responsabilité de l'idempotence vers le consommateur, pas un oubli d'implémentation.
La question qui détermine si Kafka est justifié
Avant d'introduire Kafka sur un projet, la question à se poser n'est pas « est-ce que ça peut être utile », mais « est-ce que j'ai vraiment plusieurs consommateurs indépendants qui doivent réagir au même événement, avec des rythmes de traitement différents ». Si la réponse est un seul consommateur synchrone, un appel de fonction classique reste plus simple, plus facile à tester, et plus facile à faire évoluer. Kafka se justifie quand le découplage résout un vrai problème d'organisation ou d'échelle, pas comme choix par défaut d'architecture moderne.
Ce que ça implique pour les tests
Sur MissionMatch, chaque consommateur d'événement est testé indépendamment du producteur, avec un événement construit directement en mémoire plutôt qu'en passant par un vrai broker Kafka dans les tests unitaires. C'est la même discipline hexagonale qui s'applique ici : le domaine ne doit pas savoir qu'il communique via Kafka, seulement qu'il publie un événement à travers une interface. Le broker concret reste un détail d'infrastructure, substituable, testable en isolation.