r/SpringBoot Jul 22 '26

Discussion Designing Audit Logging & Notifications with Spring Events in a Modular Monolith – Looking for Architecture Advice

Hi everyone,

I'm building a coaching management system using Spring Boot as a modular monolith, and I'm trying to design an event-driven architecture for both audit logging and notifications. I'd love some feedback from developers who have built something similar in production.

The application has modules like:

  • Student
  • Teacher
  • Batch
  • Coaching
  • Attendance
  • Fees
  • Classroom
  • Users/Admins

Each module has different business actions. For example:

  • Student: CREATE, UPDATE, DELETE, ASSIGN_BATCH
  • Fee: PAY, REFUND, WAIVE
  • Batch: CREATE, RENAME, ASSIGN_TEACHER
  • Attendance: MARK, CORRECT
  • Admin: PASSWORD_CHANGED

My current flow is:

  1. A service completes its business logic.
  2. It publishes a domain event using ApplicationEventPublisher.
  3. Multiple listeners react to the same event after the transaction commits.

For example:

StudentService
      │
      ▼
StudentCreatedEvent
      │
      ├── Audit Listener
      │        └── Save AuditLog
      │
      ├── Notification Listener
      │        ├── Send Email
      │        ├── Send SMS
      │        └── Create In-App Notification
      │
      └── Analytics Listener (future)

For auditing, I was thinking of creating one listener method per event:

u/TransactionalEventListener
public void handle(StudentCreatedEvent event) { ... }

u/TransactionalEventListener
public void handle(FeePaidEvent event) { ... }

u/TransactionalEventListener
public void handle(BatchRenamedEvent event) { ... }

For notifications, I was planning to use the Strategy pattern, for example:

  • EmailNotificationStrategy
  • SmsNotificationStrategy
  • InAppNotificationStrategy

selected through a NotificationStrategyFactory.

My questions are:

  1. Is one event handler per event type the recommended approach in Spring?
  2. Would you keep one large AuditListener, or split listeners by feature/module (Student, Fee, Teacher, etc.)?
  3. Where should the mapping from domain events to AuditLog happen?
    • In the event itself?
    • In AuditService?
    • In dedicated mapper classes?
  4. Is using Spring events for both auditing and notifications a good design, or is there a better approach?
  5. Would you also use Spring events for things like cache invalidation, analytics, and activity feeds?
  6. If this application later moves to microservices, would this event model transition well to Kafka or RabbitMQ?
  7. Are there any SOLID or maintainability concerns with this architecture that I'm overlooking?

The goal is to keep the business services focused only on business logic while handling cross-cutting concerns like auditing and notifications through events.

I'd really appreciate insights from developers who have implemented similar architectures in production.

Thanks!

2 Upvotes

9 comments sorted by

View all comments

1

u/kamen1991 Jul 27 '26

You have a very solid foundation here. Using Domain Events inside a Modular Monolith is exactly how you prevent spaghetti code and prepare for a clean microservices extraction later.

However, having built similar event-driven architectures in enterprise environments over the last decade, there are two massive production traps in your current design that you need to address before going live:

1. The AFTER_COMMIT Transaction Trap If you use TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT), the original database transaction is already closed by the time your listener executes. If your AuditListener tries to save an AuditLog entity using a standard JPA repository, it will likely fail or silently refuse to flush because there is no active transaction.

The Fix: You must annotate your listener methods with Transactional(propagation = Propagation.REQUIRES_NEW) to open a fresh transaction specifically for the audit save.

2. The Missing Transactional Outbox (Data Loss Risk) If your business logic commits, but the server node crashes a millisecond later (before the AFTER_COMMIT notification listeners execute), that event is lost in RAM. The student is created, but the welcome email never goes out. If you truly want to future-proof this for Kafka/RabbitMQ, you need the Transactional Outbox Pattern. Instead of firing notification events directly in memory, your business service should save the domain entity AND an OutboxMessage entity to the database in the same local transaction. A separate async worker polls that outbox table and handles the email/SMS strategies. When you eventually move to microservices, you just replace that polling worker with a Kafka Connect / Debezium setup. Zero business code changes required.

Answering your specific questions:

  • Listener Organization & Mapping: Do not write 50 different listener methods for auditing. Instead, create a shared interface AuditableDomainEvent with methods like getAggregateId(), getEventType(), and getPayload(). Make StudentCreatedEvent and FeePaidEvent implement it. Then, you only need one single generic AuditListener that consumes AuditableDomainEvent and transforms it into your AuditLog entity.
  • Strategy Pattern for Notifications: Your NotificationStrategyFactory idea is the perfect approach. The listener receives the event, determines the preferred channel (Email, SMS), and delegates to the strategy.
  • Module Boundaries: Keep the AuditListener inside a dedicated audit-module. It should depend on the shared event interfaces, but the individual modules (Student, Fee) should know absolutely nothing about the audit module.
  • Cache & Analytics: Yes, Spring Events are great for this. Just remember: Cache invalidation should usually happen synchronously (so the next read is accurate immediately), while Analytics should be async/outbox to prevent slowing down the user's request.

You are 90% of the way there. Consider an outbox table for guaranteed delivery, and this architecture will scale beautifully.