Skip to main content

0057: Swappable Notification Channels

Date: 2025-12-04

Status: Accepted

Context

The notification-service must support multiple modes of communication (Email, SMS, WebSocket) and potentially multiple providers for each mode (e.g., switching from SendGrid to AWS SES, or Twilio to MessageBird). Tightly coupling the core logic to a specific provider's SDK would make the system brittle and difficult to adapt to changing business needs or costs.

Decision

We will apply the Ports and Adapters (Hexagonal) pattern to notification delivery.

  1. The Port (Interface): The application layer will define a generic Channel interface.

    type Channel interface {
    Send(ctx context.Context, notification *Notification) error
    Supports(channelType ChannelType) bool
    }
  2. The Adapters (Infrastructure): Concrete implementations will reside in the infrastructure layer:

    • EmailChannel: Uses github.com/wneessen/go-mail to support SMTP and XOAUTH2. It will support CID Embedding for images to ensure emails render correctly in offline/hybrid environments without external CDNs.
    • SmsChannel:
      • Generic Webhook Adapter: The primary extension point. It POSTs a standard JSON payload to a configured URL. This allows integrating with any SMS provider via a simple serverless function or bridge, keeping the core binary lightweight.
      • Native Adapters: Optional, built-in adapters for market leaders (e.g., Twilio) where the SDK overhead is justified.
    • WebsocketChannel: A "Push" adapter that publishes the notification payload to a NATS subject (events.user.{id}.notification). It does not hold WebSocket connections itself; it relies on the NATS infrastructure to handle the active connections to frontend clients.
    • LogChannel: A dev-only adapter that prints to stdout.
  3. Factory Pattern: A ChannelFactory will be used to instantiate and retrieve the correct adapter at runtime based on the channel field in the notification request and the configured profile.

  4. NATS Auth Callout (WebSockets): For the WebSocket channel, security is handled by NATS delegating authentication to the iam-service via an Auth Callout. The notification-service is only responsible for publishing the message to the correct subject.

Consequences

Positive

  • Vendor Agnostic: We can switch providers by changing configuration, not code.
  • Testability: We can easily mock the Channel interface for unit tests, or use the LogChannel for local integration testing.
  • Extensibility: Adding a new channel type (e.g., Push Notifications) only requires implementing the interface and registering it in the factory.
  • Lightweight Binary: By prioritizing the Generic Webhook adapter for SMS, we avoid bloating the binary with dozens of provider-specific SDKs.

Negative

  • Abstraction Overhead: The generic interface may not support every specialized feature of a specific provider (e.g., Twilio-specific template logic).
  • Configuration Complexity: Managing profiles and adapters requires a structured configuration file.