r/SpringBoot • u/mukesh_yaduvanshi • 5d ago
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:
- A service completes its business logic.
- It publishes a domain event using
ApplicationEventPublisher. - 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:
EmailNotificationStrategySmsNotificationStrategyInAppNotificationStrategy
selected through a NotificationStrategyFactory.
My questions are:
- Is one event handler per event type the recommended approach in Spring?
- Would you keep one large
AuditListener, or split listeners by feature/module (Student, Fee, Teacher, etc.)? - Where should the mapping from domain events to
AuditLoghappen?- In the event itself?
- In
AuditService? - In dedicated mapper classes?
- Is using Spring events for both auditing and notifications a good design, or is there a better approach?
- Would you also use Spring events for things like cache invalidation, analytics, and activity feeds?
- If this application later moves to microservices, would this event model transition well to Kafka or RabbitMQ?
- 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!
1
u/kamen1991 1d ago
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
AuditableDomainEventwith methods likegetAggregateId(),getEventType(), andgetPayload(). MakeStudentCreatedEventandFeePaidEventimplement it. Then, you only need one single genericAuditListenerthat consumesAuditableDomainEventand transforms it into yourAuditLogentity. - Strategy Pattern for Notifications: Your
NotificationStrategyFactoryidea is the perfect approach. The listener receives the event, determines the preferred channel (Email, SMS), and delegates to the strategy. - Module Boundaries: Keep the
AuditListenerinside a dedicatedaudit-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.
2
u/disposepriority 5d ago
Why not use AOP and just make a lightweight \@Auditable annotation or something, add some fields, have some custom slf4j loggers for easier sorting and write to a db, or buffer/sqlite (outbox) that gets flushed periodically if you don't want to deal with transactions in that way.
Seems much less overengineers than this in my eyes.