Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Event Driven Notification Service

A Java backend service that processes domain events and dispatches notifications to recipients across multiple channels (email, SMS, push). Built as a portfolio project to demonstrate Clean Architecture, SOLID principles, and Gang-of-Four design patterns.


Architecture

The project follows Clean Architecture, organized into four layers with dependencies pointing inward only.

entities  ←  use cases  ←  adapters  ←  infrastructure
Layer Package Responsibility
Entities entities/ Core domain objects: Event, Notification, Recipient
Use Cases usecases/ Application logic: publish events, retry failures, query notifications
Adapters adapters/ Channel handlers, observers, controller
Infrastructure infrastructure/ In-memory repository implementations

Dependency Inversion on the Dispatcher

A key architectural decision was keeping the use case layer free of any dependency on channel handler implementations. Rather than having PublishEventUseCaseImpl call ChannelHandlerFactory directly — which would make the use case depend on the adapters layer — a NotificationDispatcher port is defined in usecases/ports/. The adapters layer provides ChannelHandlerDispatcher as the concrete implementation, wired in at startup. This mirrors the same pattern used for repositories.


Design Patterns

Builder

Event, Notification, and Recipient all use inner Builder classes. Separates complex object construction from the domain class itself and produces immutable-by-design instances with sensible defaults (e.g. PENDING status, auto-generated UUIDs).

Factory

ChannelHandlerFactory maps a NotificationChannel enum value to the appropriate NotificationChannelHandler implementation. Adding a new channel requires only a new handler class and a single new case in the factory switch expression — no changes to calling code.

Strategy

NotificationChannelHandler is the strategy interface. EmailChannelHandler, SmsChannelHandler, and PushChannelHandler are interchangeable implementations. The dispatcher selects the correct strategy at runtime based on a recipient's notification preferences.

Observer

PublishEventUseCaseImpl maintains lists of NotificationObserver and RecipientObserver subscribers. Observers are notified after each dispatch, decoupling audit logging and activity tracking from the core publish logic. NotificationObserver fires on every dispatch regardless of outcome; RecipientObserver fires only on success, since a failed dispatch does not constitute real recipient activity.


Use Cases

PublishEventUseCase — Accepts a domain event, resolves the recipient, creates a Notification per channel preference, dispatches each one, and updates the notification status based on the result.

RetryFailedNotificationUseCase — Finds all FAILED notifications for a recipient and re-dispatches any that have not exceeded MAX_RETRY_COUNT (3). Increments the retry count on each attempt regardless of outcome.

GetNotificationsByRecipientUseCase — Returns all notifications for a given recipient ID.

ManageRecipientUseCase — Adds and removes recipients from the repository.


Testing

Unit tests are written with JUnit 5. Dependencies are replaced with hand-rolled stubs rather than a mocking framework, keeping the tests lightweight and the stubs explicit. Each use case is tested in isolation with coverage across happy paths, error cases, and edge cases (bad recipient IDs, max retry exceeded, failed dispatch, observer trigger behaviour).


Running the Project

The Main class wires all dependencies manually and runs a demo scenario: a recipient is registered with an email preference, an ORDER_CONFIRMED event is published, and the notification is dispatched.

Using javac and running manually:

# Requires Java 17+
javac -d out $(find src/main -name "*.java")
java -cp out com.example.notificationservice.Main

or

Using Gradle:

gradlew.bat run

Run the tests:

gradlew.bat test

What Would Come Next

This project intentionally keeps infrastructure simple to keep the focus on architecture and patterns. In a production context the natural next steps would be:

  • Persistence — swap the in-memory repository implementations for a relational database (e.g. MySQL with JDBC or JPA) without touching the use case layer
  • REST API — expose the use cases via Spring MVC controllers, with the existing NotificationController as a starting point
  • Real channel integrations — replace the stub handlers with calls to SendGrid (email), Twilio (SMS), and Firebase (push)
  • Async dispatch — move notification dispatch to a message queue (e.g. Kafka or RabbitMQ) for resilience and throughput
  • Retry scheduling — trigger RetryFailedNotificationUseCase on a schedule rather than on demand

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages