Skip to content

Notification Service

Multi-channel notification delivery platform. Other services publish to a named topic; the notification service fans out to all channels configured for that topic — Firebase FCM push, STOMP/WebSocket, and HTTP (SSE via Redis ZSet).

Runtime: Spring Boot 3.5.7 / Java 21 · 3 pods · nexivo namespace
Image: communication-services/notification-service


Tech Stack

Component Detail
Framework Spring Boot 3.5.7, Java 21
Database PostgreSQL
Cache / Pub-Sub / SSE Store Redis
Push Notifications Firebase Admin SDK 9.2.0 (FCM)

Delivery Channels

Channel Type Mechanism Target Notes
PUSH_NOTIFICATION Firebase FCM Device token or FCM topic TTL per platform; auto-removes unregistered tokens
WEBSOCKET STOMP over SockJS /user/{userId}/{topic} (unicast) or /topic/{topic} (broadcast) User identified by X-USER-ID header
HTTP SSE via Redis ZSet Connected GET /me/notifications clients Missed events replayed on reconnect via Last-Event-ID

Send Flow

sequenceDiagram
    participant Caller as Calling Service
    participant NS as NotificationService
    participant FCM as Firebase FCM
    participant STOMP as STOMP Broker
    participant Redis
    participant SSE as SSE Client

    Caller->>NS: POST /notifications/{topic}?subscriber=userId

    NS->>NS: Resolve NotificationSettings for topic
    NS->>NS: Fan-out to all ChannelConfigs

    alt PUSH_NOTIFICATION channel
        NS->>FCM: Send to device token(s) or FCM topic
        FCM-->>NS: Delivery receipt
        NS->>NS: Auto-remove unregistered tokens on error
    end

    alt WEBSOCKET channel
        NS->>STOMP: convertAndSendToUser(userId, /topic, payload)
    end

    alt HTTP channel
        NS->>Redis: ZADD notifications:{userId}:{channelId} <timestamp> <payload>
        Redis-->>SSE: PUBLISH notifications channel event
        SSE-->>SSE: Pop ZSet entries, push to emitter
    end

Key Entities

NotificationChannel

A reusable, named delivery endpoint definition.

Field Type Notes
name String Human-readable identifier
channelType Enum WEBSOCKET, PUSH_NOTIFICATION, HTTP

NotificationSettings

Binds a unique topic to a TTL and one or more channel configs.

Field Type Notes
topic String Unique; used in POST /notifications/{topic}
ttl Integer (seconds) Message time-to-live
channelConfigs 1:M → ChannelConfig Delivery targets

ChannelConfig (single-table inheritance)

Subtype Extra Fields
PushNotificationConfig title, body, priority (HIGH / NORMAL)
HttpConfig (base fields only)
WebsocketConfig (base fields only)

FcmTokenEntity

Field Type Notes
token String Unique; FCM registration token
userId UUID Owning user
platformType Enum ANDROID, IOS, WEB

REST API

Notification Dispatch

Method Path Description
POST /notifications/{topic} Send notification to topic. Optional ?subscriber={userId} for unicast; omit for broadcast.

Channel Definitions — /notification-channels

Method Path Description
GET /notification-channels List channels
POST /notification-channels Create channel
GET /notification-channels/{id} Get channel
PUT /notification-channels/{id} Update channel
DELETE /notification-channels/{id} Delete channel

Topic Settings — /notification-settings

Method Path Description
GET /notification-settings List topic settings
POST /notification-settings Create topic + channel configs
GET /notification-settings/{id} Get topic settings
PUT /notification-settings/{id} Update topic settings
DELETE /notification-settings/{id} Delete topic settings

Device Tokens — /me/fcm-tokens

Method Path Description
POST /me/fcm-tokens Register FCM device token
GET /me/fcm-tokens List tokens for current user
DELETE /me/fcm-tokens/{token} Remove a token

SSE Subscription — /me/notifications

Method Path Description
GET /me/notifications Subscribe to live notification stream (text/event-stream)

SSE Subscription Detail

The GET /me/notifications endpoint returns a text/event-stream response backed by a SseEmitter.

Behaviour:

  • Timeout: 30 minutes per connection.
  • Heartbeat: every 5 seconds (configurable via notification.heartbeat.interval-ms).
  • Last-Event-ID support: on reconnect, the service replays any ZSet entries with a score (timestamp) greater than the supplied ID, recovering missed notifications.
  • Concurrency: a ReentrantLock per channel prevents concurrent emit races across multiple threads.
  • Re-queue logic: a notification is only re-queued into the Redis ZSet if all registered emitters for the user fail to deliver.

Redis ZSet key pattern: notifications:{userId}:{channelId}
ZSet score = Unix timestamp (ms). TTL mirrors NotificationSettings.ttl.


Firebase FCM Delivery Notes

  • Tokens are stored per platformType; TTL is applied per platform:
    • APNS (iOS): apns-expiration header set from TTL.
    • Android: ttl field in FCM message (milliseconds).
    • Web push: TTL header in Webpush config.
  • On any FCM error indicating an unregistered or invalid token, the FcmTokenEntity row is automatically deleted.
  • Topic broadcast (no subscriber param) sends to the FCM topic matching the notification topic name.

Key Configuration

Property Default / Example Description
notification.firebase.service-account-key-json firebase.json (dev) · /etc/secrets/firebase.json (k8s) Path to Firebase service account JSON
notification.heartbeat.interval-ms 5000 SSE heartbeat interval in milliseconds
Redis (standard Spring Redis config) Used for SSE ZSet storage and STOMP broker relay