@@ -20,6 +20,7 @@ role-based access control (ADMIN/MEMBER), and PostgreSQL persistence with Flyway
2020- ** Java 25** with virtual threads (Project Loom)
2121- ** Spring Boot 4.0** with Spring Framework 7.0
2222- ** Spring Modulith 2.0** for modular architecture
23+ - ** Apache Kafka** for event externalization (Spring Modulith integration)
2324- ** PostgreSQL** with Flyway migrations
2425- ** gRPC** alongside REST APIs
2526
@@ -187,7 +188,8 @@ notification ──→ shared ←── user
187188** Notification Module** (` org.nkcoder.notification ` ):
188189
189190- ` NotificationService ` - Public API for sending notifications
190- - ` application/UserEventListener ` - Listens to UserRegisteredEvent
191+ - ` application/ApplicationEventListener ` - In-process listener for domain events (sends emails)
192+ - ` application/KafkaEventListener ` - Kafka consumer for externalized events
191193
192194** Shared Module** (` org.nkcoder.shared ` ):
193195
@@ -329,34 +331,54 @@ PATCH /api/users/{userId}/password - Reset password (admin only)
329331
330332### Event-Driven Communication
331333
332- Modules communicate via domain events using Spring Modulith's event infrastructure:
334+ Modules communicate via domain events using Spring Modulith's event infrastructure with ** Kafka externalization** .
335+
336+ ** Event Externalization** : Domain events marked with ` @Externalized ` are automatically published to Kafka topics:
337+
338+ | Event | Kafka Topic | Description |
339+ | -------| -------------| -------------|
340+ | ` UserRegisteredEvent ` | ` user.registered ` | Published when user completes registration |
341+ | ` OtpRequestedEvent ` | ` user.otp.requested ` | Published when user requests OTP |
333342
334343** Publishing Events** (in User module):
335344
336345``` java
337346// In AuthApplicationService after registration
338- domainEventPublisher. publish(new UserRegisteredEvent (user. getId(),user.
347+ domainEventPublisher. publish(new UserRegisteredEvent (user. getId(), user. getEmail(), user. getName()));
348+ ```
339349
340- getEmail(),user.
350+ ** Event Definition with Kafka Externalization ** :
341351
342- getName()));
352+ ``` java
353+ @Externalized (" user.registered" ) // Kafka topic name
354+ public record UserRegisteredEvent(UUID userId, String email, String userName, LocalDateTime occurredOn)
355+ implements DomainEvent {}
343356```
344357
345358** Listening to Events** (in Notification module):
346359
347360``` java
348-
361+ // In-process listener (immediate, same JVM)
349362@Component
350- public class UserEventListener {
363+ public class ApplicationEventListener {
351364 @ApplicationModuleListener
352365 public void onUserRegistered (UserRegisteredEvent event ) {
353366 notificationService. sendWelcomeEmail(event. email(), event. userName());
354367 }
355368}
369+
370+ // Kafka consumer (for external/distributed processing)
371+ @Component
372+ public class KafkaEventListener {
373+ @KafkaListener (topics = " user.registered" , groupId = " notification-service" )
374+ public void onUserRegistered (String message ) {
375+ // Decode Base64 and deserialize JSON
376+ }
377+ }
356378```
357379
358380** Event Publication Table** : Spring Modulith persists events to ` event_publication ` table for reliable delivery (
359- transactional outbox pattern).
381+ transactional outbox pattern). Events are stored before being sent to Kafka, ensuring at-least-once delivery.
360382
361383### Configuration Management
362384
@@ -378,6 +400,7 @@ JWT_REFRESH_SECRET=<min 64 bytes for HS512>
378400JWT_ACCESS_EXPIRES_IN=15m
379401JWT_REFRESH_EXPIRES_IN=7d
380402CLIENT_URL=http://localhost:3000
403+ SPRING_KAFKA_BOOTSTRAP_SERVERS=kafka:9092
381404```
382405
383406** Configuration Binding** :
@@ -564,9 +587,10 @@ class ModulithArchitectureTest {
564587
5655881 . Create event record in ` shared/kernel/domain/event/ ` (if cross-module) or ` {module}/domain/event/ ` (if
566589 module-internal)
567- 2 . Inject ` DomainEventPublisher ` in your service
568- 3 . Call ` domainEventPublisher.publish(event) ` after business logic
569- 4 . Create ` @ApplicationModuleListener ` in consuming module
590+ 2 . Add ` @Externalized("topic-name") ` annotation to publish to Kafka
591+ 3 . Inject ` DomainEventPublisher ` in your service
592+ 4 . Call ` domainEventPublisher.publish(event) ` after business logic
593+ 5 . Create ` @ApplicationModuleListener ` in consuming module (in-process) and/or ` @KafkaListener ` (Kafka consumer)
570594
571595** Database Schema Change** :
572596
@@ -604,11 +628,20 @@ class ModulithArchitectureTest {
604628- Cross-module events go in ` shared.kernel.domain.event/ `
605629- Use ` @ApplicationModuleListener ` for reliable event handling (auto-retry, persistence)
606630
631+ ** Kafka Integration** :
632+
633+ - Events with ` @Externalized ` annotation are automatically published to Kafka topics
634+ - Consumer group: ` notification-service `
635+ - Messages are Base64-encoded JSON
636+ - Kafka ports: ` 9092 ` (internal Docker), ` 29092 ` (external/host)
637+ - Topics are auto-created on first publish
638+
607639** Future Microservice Extraction** :
608640When ready to extract a module as a microservice:
609641
610- 1 . Events become messages ( Kafka/RabbitMQ)
642+ 1 . Events are already externalized to Kafka - no change needed
6116432 . REST/gRPC calls replace direct method calls
6126443 . Module's ` infrastructure/ ` adapters change, domain stays the same
6136454 . Database can be separated per module
646+ 5 . Kafka consumers in extracted service continue to receive events
614647
0 commit comments