Domain / Business
Events / Async
Infrastructure
External Services
Security / Auth
Data / Storage
Domain Model + Template Engine
«aggregate root»Notification
- notificationId: NotificationId (UUID)
- clientId: ClientId
- type: NotificationType (enum)
- channel: PUSH | EMAIL | SMS | IN_APP | SSE
- priority: HIGH | NORMAL | LOW
- templateKey: String
- locale: Locale (en_IN | hi_IN | ta_IN)
- variables: Map<String, String>
- status: PENDING | SENT | DELIVERED | FAILED | READ
- sentAt: Instant?
- deliveredAt: Instant?
- readAt: Instant?
- retryCount: Int (max 3)
+ send(): DomainEvent[NotificationSent]
+ markDelivered(): DomainEvent[NotificationDelivered]
+ markRead(): void
+ fail(reason): DomainEvent[NotificationFailed]
+ retry(): void (increments retryCount)
«domain service»TemplateEngine
- templates: Map<TemplateKey, Map<Locale, Template>>
- variableResolvers: List<VariableResolver>
+ resolve(templateKey, locale, vars): RenderedContent
+ validateTemplate(key): ValidationResult
+ listTemplates(): List<TemplateMetadata>
Notification Types (enum)
ORDER_PLACED ORDER_CONFIRMED SIP_EXECUTED PAYMENT_RECEIVED TIER_UPGRADED KYC_VERIFIED GOAL_MILESTONE MARKET_ALERT REBALANCE_DUE WILL_REMINDER INSURANCE_QUOTE PAYMENT_FAILED
Multi-Channel Delivery Pipeline
Domain Event → Channel Strategy → Template Resolve → Dispatch per Channel
Domain Event
Kafka consumer
byld.distribution.events
.order-confirmed etc.
Channel Strategy
Resolve channels by
event type + user prefs
+ priority rules
Template Resolve
templateKey + locale
→ rendered content
variable substitution
Dispatch
Fan-out to selected
channel adapters
async non-blocking
🔔
Push (FCM)
Firebase Cloud Messaging
Device token registry
Topic-based for broadcasts
p99 < 500ms delivery
Supports rich notifications
Email (SES)
Amazon SES
Templated HTML emails
DKIM/SPF configured
Bounce/complaint handling
GST invoices as attachment
📱
SMS (SNS)
Amazon SNS
Transactional route
DLT registration (TRAI)
Template pre-approved
OTP + critical alerts only
💬
In-App / SSE
Spring SseEmitter
Real-time notification bell
Persisted in Redis (7d)
Read/unread tracking
Badge count management
«domain service»ChannelStrategy
+ resolveChannels(event, preferences): List<Channel>
Rules: HIGH priority → all enabled channels | NORMAL → push + in-app | LOW → in-app only
Override: user preference always wins (except regulatory: OTP must go SMS)
«port»NotificationSender
+ send(notification): DeliveryResult
+ checkStatus(id): DeliveryStatus
Implementations
FcmPushSender (Firebase)
SesEmailSender (AWS SES)
SnsSmsSender (AWS SNS)
SseInAppSender (Spring)
i18n Template Resolution
Event
type: ORDER_CONFIRMED
vars: {scheme, units}
+
Locale
user.locale
en_IN | hi_IN | ta_IN
Rendered
Final message
ready for delivery
Template: order_confirmed.push (en_IN)
Your order for {{units}} units of {{scheme}} has been confirmed. NAV: {{nav}} | Amount: {{amount}}
Template: order_confirmed.push (hi_IN)
{{scheme}} ke {{units}} units ka order confirm ho gaya hai. NAV: {{nav}} | Amount: {{amount}}
Template: sip_executed.email (en_IN)
Subject: SIP Executed - {{scheme}} --- Hi {{name}}, Your SIP of {{amount}} for {{scheme}} has been executed successfully. Units allotted: {{units}} at NAV {{nav}} Next SIP date: {{nextDate}}
Template Storage
PostgreSQL: notification_templates table
Key: {event_type}.{channel}.{locale}
Versioned: template_version for A/B testing
Cache: Redis with 1h TTL (invalidate on update)
Fallback: en_IN if locale template missing
Phase 1 Locales
en_IN (English - default)
hi_IN (Hindi)
Phase 2 Locales
ta_IN (Tamil)
mr_IN (Marathi)
gu_IN (Gujarati)
User Preference Management + Delivery Log
Channel Preferences (per user)
Event Category Push Email SMS In-App
Order Updates
SIP Execution
Payment Alerts
Market Alerts
Goal Milestones
Security/OTP FORCED FORCED FORCED FORCED
Promotional
FORCED = regulatory requirement, cannot be disabled by user (SEBI/TRAI)
Delivery Log (sample)
PUSH
ORDER_CONFIRMED (HDFC Top 100)
2.3s
EMAIL
ORDER_CONFIRMED (HDFC Top 100)
4.1s
SSE
ORDER_CONFIRMED (HDFC Top 100)
0.2s
PUSH
SIP_EXECUTED (Axis Bluechip)
pending
SMS
PAYMENT_FAILED (SIP debit)
retry #2
EMAIL
TIER_UPGRADED (Edge → Legacy)
3.8s
PUSH
MARKET_ALERT (NIFTY -2%)
1.1s
Retention Policy
In-app: 90 days
Delivery logs: 30 days (hot) + 1 year (S3 cold)
Email records: 7 years (compliance / SEBI)
Architecture Notes
"The first rule of distributed systems: don't distribute."
-- Martin Fowler (but we need to, so do it cleanly)
com.byld.notifications
/domain Notification, TemplateEngine, ChannelStrategy, NotificationSender
/application SendNotificationUseCase, UpdatePreferencesUseCase, BatchDigestUseCase
/infrastructure FcmPushSender, SesEmailSender, SnsSmsSender, SseInAppSender
/api PreferencesController, NotificationController, WebhookBounceHandler
Key Decisions:
1. NotificationSender is a port with 4 implementations
2. ChannelStrategy is a domain service (business rules)
3. Template is versioned for A/B testing
4. SMS only for OTP + critical (cost control)
5. Delivery log for audit trail (SEBI compliance)
6. TRAI DLT registration for SMS templates
Database
byld_notifications (PostgreSQL + Redis)
Tables: notifications, templates,
preferences, delivery_logs, device_tokens
Kafka Topics (consumed)
byld.notifications.events.notification-send
byld.distribution.events.order-confirmed
byld.payments.events.payment-captured
byld.loyalty.events.tier-upgraded
byld.sip.events.sip-executed