Skip to Content
06 AdvancedArchitecture Event-Driven

Architecture Event-Driven

Cette page présente l’architecture event-driven dans une architecture hexagonale : events locaux, messaging asynchrone et intégration Kafka.


Pourquoi Event-Driven ?

L’architecture event-driven permet de découpler les composants en utilisant des événements au lieu d’appels directs.

Approche couplée

@Singleton @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; private final EmailService emailService; private final AuditService auditService; public User createUser(CreateUserRequest request) { User user = new User(request.email(), request.name()); User saved = userRepository.save(user); // Couplage fort : UserService dépend directement de EmailService et AuditService emailService.sendWelcomeEmail(saved.getEmail()); auditService.logUserCreation(saved.getId()); return saved; } }

Problèmes :

  • Couplage fort entre services
  • Si EmailService échoue, la création d’utilisateur échoue aussi
  • Difficile de tester UserService isolément
  • Impossible d’ajouter de nouveaux listeners sans modifier UserService

Event-Driven = Découplage + Résilience + Extensibilité


Events Locaux avec Micronaut

Les events locaux sont des événements publiés et consommés dans la même application.

Définir l’événement

Les événements sont des records Java immuables :

package com.example.domain.events; import java.time.Instant; public record UserCreatedEvent( Long userId, String email, Instant createdAt ) { public UserCreatedEvent(Long userId, String email) { this(userId, email, Instant.now()); } }

Utilisez des records pour les événements : immuables, concis, avec equals/hashCode automatiques.

Publier l’événement

Injectez ApplicationEventPublisher<T> dans votre service :

import io.micronaut.context.event.ApplicationEventPublisher; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; @Singleton @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; private final ApplicationEventPublisher<UserCreatedEvent> eventPublisher; public User createUser(CreateUserRequest request) { User user = new User(request.email(), request.name()); User saved = userRepository.save(user); // Publier l'événement eventPublisher.publishEvent( new UserCreatedEvent(saved.getId(), saved.getEmail()) ); return saved; } }

Écouter l’événement

Utilisez @EventListener pour réagir aux événements :

import io.micronaut.context.event.ApplicationEventListener; import io.micronaut.runtime.event.annotation.EventListener; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton @RequiredArgsConstructor public class UserEventListener { private final EmailService emailService; private final AuditService auditService; @EventListener public void onUserCreated(UserCreatedEvent event) { log.info("User created: {}", event.userId()); // Envoyer email de bienvenue emailService.sendWelcomeEmail(event.email()); // Audit log auditService.logUserCreation(event.userId()); } }

Les events locaux sont synchrones par défaut : le listener s’exécute dans le même thread que le publisher.


Events Asynchrones

Pour exécuter les listeners en arrière-plan, utilisez @Async.

Event synchrone

@EventListener public void onUserCreated(UserCreatedEvent event) { // S'exécute dans le thread du publisher emailService.sendWelcomeEmail(event.email()); // Bloquant }

Flux d’exécution :

createUser() → publishEvent() → onUserCreated() → sendEmail() → retour ↑_______________↓ (même thread, bloquant)

Attention : les events asynchrones ne garantissent pas l’ordre d’exécution ni la gestion des erreurs automatique.


Domain Events

Les domain events sont des événements métier publiés depuis le domain layer.

      • UserCreatedEvent.java
      • OrderPlacedEvent.java
      • PaymentProcessedEvent.java

Exemple complet

Événement métier

package com.example.domain.events; import java.math.BigDecimal; import java.time.Instant; public record OrderPlacedEvent( Long orderId, Long userId, BigDecimal totalAmount, Instant placedAt ) { public OrderPlacedEvent(Long orderId, Long userId, BigDecimal totalAmount) { this(orderId, userId, totalAmount, Instant.now()); } }

Bonne pratique : séparer les listeners par responsabilité (email, inventory, analytics).


Messaging avec Kafka

Pour communiquer entre applications, utilisez Kafka avec Micronaut Kafka.

Ajouter la dépendance

<!-- pom.xml --> <dependency> <groupId>io.micronaut.kafka</groupId> <artifactId>micronaut-kafka</artifactId> </dependency>

Configurer Kafka

# application.yml kafka: bootstrap: servers: localhost:9092 producers: default: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: io.micronaut.kafka.serde.JsonSerde consumers: default: key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: io.micronaut.kafka.serde.JsonSerde group-id: my-app auto-offset-reset: earliest

Créer le Producer

import io.micronaut.configuration.kafka.annotation.KafkaClient; import io.micronaut.configuration.kafka.annotation.Topic; @KafkaClient public interface UserEventProducer { @Topic("user-events") void sendUserCreated(UserCreatedEvent event); }

Publier l’événement

@Singleton @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; private final UserEventProducer eventProducer; public User createUser(CreateUserRequest request) { User user = new User(request.email(), request.name()); User saved = userRepository.save(user); // Publier dans Kafka eventProducer.sendUserCreated( new UserCreatedEvent(saved.getId(), saved.getEmail()) ); return saved; } }

Consommer l’événement

import io.micronaut.configuration.kafka.annotation.KafkaListener; import io.micronaut.configuration.kafka.annotation.Topic; import io.micronaut.messaging.annotation.MessageBody; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton @KafkaListener(groupId = "email-service") public class UserEventConsumer { @Topic("user-events") public void onUserCreated(@MessageBody UserCreatedEvent event) { log.info("Received UserCreatedEvent: {}", event.userId()); // Traiter l'événement emailService.sendWelcomeEmail(event.email()); } }

Kafka vs Events Locaux :

  • Events locaux : même application, synchrone ou async
  • Kafka : inter-applications, durable, scalable

Architecture Event-Driven Complète

Voici une architecture complète combinant events locaux et Kafka.

      • UserCreatedEvent.java
      • OrderPlacedEvent.java

Flux de données

1. User fait une requête HTTP 2. Controller appelle Use Case 3. Use Case publie Event Local 4. Listener Local traite l'event (audit, cache) 5. Listener Local publie dans Kafka 6. Autres microservices consomment depuis Kafka

Implémentation

Use Case

@Singleton @RequiredArgsConstructor public class CreateUserUseCase { private final UserRepository userRepository; private final ApplicationEventPublisher<UserCreatedEvent> eventPublisher; public User execute(CreateUserRequest request) { User user = new User(request.email(), request.name()); User saved = userRepository.save(user); // Publier event local eventPublisher.publishEvent( new UserCreatedEvent(saved.getId(), saved.getEmail()) ); return saved; } }

Pattern recommandé : events locaux pour les traitements internes + Kafka pour les communications inter-services.


Gestion des Erreurs

Les erreurs dans les listeners doivent être gérées avec soin.

Sans gestion d’erreurs

@EventListener public void onUserCreated(UserCreatedEvent event) { // Si sendEmail() lève une exception, elle remonte au publisher emailService.sendEmail(event.email()); // ❌ Peut échouer }

Problème : l’exception remonte et peut bloquer le publisher.

Best practice : toujours gérer les erreurs dans les listeners asynchrones pour éviter les erreurs silencieuses.


Event Sourcing (Introduction)

L’event sourcing consiste à stocker tous les événements métier au lieu de l’état final.

CRUD traditionnel

// État final stocké en BDD User user = new User(1L, "john@example.com", "active"); userRepository.save(user); // Mise à jour : écrase l'ancien état user.setStatus("inactive"); userRepository.save(user); // ❌ On ne sait pas POURQUOI le status a changé

Event Sourcing est un pattern avancé nécessitant une infrastructure dédiée (event store). Pour débuter, utilisez les events locaux et Kafka.


Tests

Tester la publication d’événements

@MicronautTest class CreateUserUseCaseTest { @Inject CreateUserUseCase useCase; @MockBean(ApplicationEventPublisher.class) ApplicationEventPublisher<UserCreatedEvent> eventPublisher() { return mock(ApplicationEventPublisher.class); } @Test void shouldPublishEventOnUserCreation() { // Given CreateUserRequest request = new CreateUserRequest("john@example.com", "John"); // When useCase.execute(request); // Then verify(eventPublisher).publishEvent(argThat(event -> event.email().equals("john@example.com") )); } }

Tester un listener

@MicronautTest class UserEventListenerTest { @Inject UserEventListener listener; @MockBean(EmailService.class) EmailService emailService() { return mock(EmailService.class); } @Test void shouldSendEmailOnUserCreated() { // Given UserCreatedEvent event = new UserCreatedEvent(1L, "john@example.com"); // When listener.onUserCreated(event); // Then verify(emailService).sendWelcomeEmail("john@example.com"); } }

Bonnes Pratiques

Règles d’or pour les events :

  1. Nommage : utiliser le passé (UserCreatedEvent, pas CreateUserEvent)
  2. Immuabilité : utiliser des record Java pour les événements
  3. Contenu : inclure uniquement les données nécessaires (pas toute l’entité)
  4. Versioning : prévoir l’évolution du schéma (ajouter des champs optionnels)
  5. Idempotence : les listeners doivent supporter le retraitement du même événement
  6. Gestion d’erreurs : toujours wrapper les listeners avec try-catch
  7. Tests : tester la publication ET la consommation des événements

Comparaison des Approches

ApprocheScopeLatenceDurabilitéScalabilitéComplexité
Events LocauxMême appBasseNonLimitéeFaible
Events AsyncMême appMoyenneNonMoyenneFaible
KafkaInter-appsMoyenneOuiHauteMoyenne
Event SourcingMême appHauteOuiHauteHaute

Références


Prochaine étape : Ports GraphQL

Last updated on