this project is our notification system. it handles sending emails, sms messages, and push notifications. the goal was to build something reliable and flexible so we can easily swap out external services if needed.
from the start, i wanted something robust and flexible. that's why i went for a hexagonal architecture approach. imagine it like this:
the core brain: this is where our actual notification logic lives. it knows what a notification is, how to decide if it should be sent, and what it should look like. it doesn't care how it gets sent or where the data comes from. it just focuses on the business of notifying.
the action plans: this layer takes requests (like "send a welcome email") and orchestrates the steps. it talks to the brain for decisions and then asks the outside world(specific connections in this case) to do the actual sending or saving.
the outside connections: these are the parts that actually plug into real-world services like databases, email senders, sms provider, or push notification services. the cool thing is, our brain and action plans don't know or care about the specifics of these, they just use the 'connections'. this means we can swap out one service for another service without touching our core logic.
the idea was to practice the concept of DDD, whilst introducing a bit of complexity whilst applying it. for this use case, the separation keeps everything tidy and makes it much easier to test and adapt.
when a request comes in (like "send this notification"), it first hits caddy, which is our reverse proxy. caddy takes care of things like https and routing, and then passes the request along to our notification service. inside the notification service, the request goes through a few layers:
- rest api layer: this is where we handle incoming http requests. it takes the data, does some basic validation, and hands it off to the business logic.
- business layer: here’s where the real work happens. this layer figures out what needs to be done—should we send an email, an sms, a push notification, or maybe all three? it also applies any business rules, like checking user preferences or rate limits.
- domain layer: this is the heart of the system. it knows what a notification is, what it means to send one, and how to track its status. it’s totally unaware of how things get sent out or stored.
- persistence layer: when we need to save or fetch data (like notification history or user preferences), this layer talks to the database. it keeps the rest of the system blissfully ignorant of sql or database details.
once the business logic decides what to do, it uses the output ports to actually send the notification. these are like plug sockets—the business logic just plugs in and doesn’t care what’s on the other end. the actual sending is handled by adapters that talk to real-world services:
- ses for email
- mnotify for sms
- firebase cloud messaging for push notifications
if we ever want to swap out one of these services, we just change the adapter
the database keeps track of everything that’s sent, so we can check statuses, retry failures, and keep a history. all of this runs inside docker containers, so it’s easy to spin up, test, and deploy anywhere. in short: requests come in, get routed, processed, and sent out through whatever channel is needed, all while keeping the core logic clean and easy to maintain.
- java 17
- maven
- docker
- git
- spring boot
- thymleaf
- tailwindcss
- postgresql(prod) and h2(dev)
- caddy(prod)
- aws ses
- mnotify sms
- firebase cloud messaging
# database
SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/notification_service
SPRING_DATASOURCE_USERNAME=postgres
SPRING_DATASOURCE_PASSWORD=password
# aws ses (email)
AWS_SES_ACCESS_KEY=your-aws-access-key
AWS_SES_SECRET_KEY=your-aws-secret-key
AWS_SES_REGION=us-east-1
[email protected]
# mnotify (sms)
MNOTIFY_API_KEY=your-mnotify-api-key
MNOTIFY_API_URL=https://api.mnotify.com/api
MNOTIFY_SENDER=YourApp
# firebase (push)
FIREBASE_PROJECT_ID=your-firebase-project-id
FIREBASE_CREDENTIALS_PATH=/app/firebase-credentials.json
# app
SPRING_PROFILES_ACTIVE=prod
To run this service in production, you need to provide credentials for AWS SES (Email), MNotify (SMS), and Firebase Cloud Messaging (Push). Here’s what you need to set up and where to put it.
- Docs: https://aws.amazon.com/ses/
- You need:
- AWS_SES_ACCESS_KEY
- AWS_SES_SECRET_KEY
- AWS_SES_REGION (e.g. us-east-1)
- AWS_SES_FROM_ADDRESS (must be a verified sender)
- Put these in your
production.env
or as environment variables in Docker Compose:AWS_SES_ACCESS_KEY=your-access-key AWS_SES_SECRET_KEY=your-secret-key AWS_SES_REGION=us-east-1 AWS_SES_FROM_ADDRESS=[email protected]
- Docs: https://readthedocs.mnotify.com/#tag/SMS
- You need:
- MNOTIFY_API_KEY (from your MNotify dashboard)
- MNOTIFY_API_URL (usually https://api.mnotify.com/api)
- MNOTIFY_SENDER (your registered sender name)
- Put these in your
production.env
or as environment variables:MNOTIFY_API_KEY=your-mnotify-key MNOTIFY_API_URL=https://api.mnotify.com/api MNOTIFY_SENDER=YourSenderName
- Docs: https://firebase.google.com/docs/admin/setup
- You need:
- A Firebase service account JSON file
- How to get it:
- Go to the Firebase Console
- Project Settings > Service Accounts
- Generate new private key and download the JSON
- Place the file in your project root as
firebase-service-account.json
(or whatever you want to call it) - In Docker Compose, mount it and set the env var:
volumes: - ./firebase-service-account.json:/app/classes/notification-system.json:ro environment: - GOOGLE_APPLICATION_CREDENTIALS=/app/classes/notification-system.json
- Make sure your credential fields above are set in
production.env
and your Firebase JSON is in place. - Start the stack:
docker-compose down -v docker-compose up --build
- The API will be at
http://localhost:8080/api
- Swagger UI:
http://localhost:8080/api/swagger-ui.html
or/api/swagger-ui/index.html
- Thymeleaf UI:
http://localhost:8080/api/templates
If you miss any credential or set a wrong value, the service will fail to send through that channel and you’ll see a clear error in the logs or API response.