/

Developer Guide

Notification Service Architecture

Bounded context: Alerting · Stack: Node.js / Express · Code-level walkthrough: Notification Service

The notification-service realises the Alerting context. Architecturally it is a rule engine and dispatcher: it decides whether an event warrants an alert and fans it out to the right people across more than one channel.


Architectural Style

A layered architecture — Router → Controller → Service → Model — organised around two responsibilities that map directly onto the context’s two aggregates: managing where a principal can be reached, and what they have chosen to receive.

The service has two entry points into the same service layer, not just one. HTTP requests arrive through router.tscontroller/notificationController.ts in the usual way; temperature alerts also arrive through services/eventListener.ts, a Redis subscriber on alerts:temperature — a driving path with no controller in front of it, since there’s no HTTP request to adapt. Both entry points now go through the same cooldown/domain-resolution/dual-delivery shape in notificationService.ts.

graph TD
    subgraph Router
        R[router.ts]
    end
    subgraph "Controller — notificationController.ts"
        ALERT["triggerAlert · pushTemperatureAlert"]
        PREF["subscribe · getPreferences · updatePreference"]
    end
    subgraph "Service — notificationService.ts / pushService.ts"
        NSV["publishNotification · sendTemperatureAlert ·\ndeliverTemperatureAlertToDomains ·\nisTemperatureAlertOnCooldown / setTemperatureAlertCooldown"]
        PSV["subscribeUser · sendPushToDomain · preference CRUD"]
    end
    subgraph Model
        M1[Delivery Channel Model]
        M2[Alert Preference Model]
    end
    EL["Event Listener\nRedis subscriber: alerts:temperature"] --> NSV
    EL --> TW
    R --> ALERT
    R --> PREF
    ALERT --> TW[twin-service: resolve domains]
    ALERT --> NSV
    ALERT --> PSV
    NSV --> PSV
    PREF --> PSV
    PSV --> M1
    PSV --> M2
    M1 -.-> DB[(notification-db)]
    M2 -.-> DB
    NSV -.->|publish| BR[(broker)]

POST /trigger, POST /push/temperature, and the alerts:temperature Redis subscription — the path that actually fires for every automatic sensor-driven threshold breach (see Communication & Data Flow’s Threshold Alert flow) — all resolve recipients via twin-service, debounce, and dispatch on both channels. The two HTTP routes forward the calling user’s own x-gateway-claims header to twin-service’s building→domain lookup; the event-listener path has no HTTP caller to forward a token from, so it presents a fixed system identity instead (getDomainsForBuilding’s default claimsHeader, in notificationService.ts) — safe only because that specific twin-service route (GET /domain/:building_name) doesn’t perform any per-identity authorization, it just requires a structurally valid claims header to exist.

The event-listener path degrades gracefully instead of failing loudly

pushTemperatureAlert throws a ValidationError (400) if no domain can be determined — reasonable for a synchronous HTTP call, where silence would be bad UX. The event-listener path can’t do that (there’s no HTTP response to fail), so if the twin-service lookup errors or resolves to zero domains, it falls back to the old unscoped broadcast (sendTemperatureAlert straight to the notifications channel) rather than dropping the alert entirely. The 5-minute cooldown still applies either way, so a building that can’t be resolved doesn’t flood the channel.


Key Architectural Decisions


Integration

For the throttle mechanics, VAPID delivery, the cleanup behaviour, and the API, see the Notification Service internals page.