Skip to content
Back to Articles

Architectural Blueprint: Building a Resilient Multi-Channel Notification Engine (Email, Telegram, WhatsApp)

SEPTEMBER 12, 20264 MIN READ
Architectural Blueprint: Building a Resilient Multi-Channel Notification Engine (Email, Telegram, WhatsApp)

In modern app development, relying solely on email for critical notifications is a fast track to poor user engagement. Users expect real-time updates where they spend most of their time: messaging platforms like Telegram, instant channels like WhatsApp, or traditional inboxes for long-form, formal updates.

However, writing custom, ad-hoc integrations for every single messaging platform quickly turns your codebase into spaghetti. Each platform brings its own authentication mechanisms, message formatting rules, rate limits, and failure modes.

In this guide, we will design and construct an enterprise-grade, multi-channel notification engine using TypeScript and Node.js. We'll leverage clean software architecture patterns—specifically the Strategy and Factory patterns—coupled with queue-based asynchronous execution to deliver notifications reliably via Email, Telegram, and WhatsApp.


Architectural Overview

Before diving into code, let's establish high-level requirements. A robust notification engine must:

  1. Decouple Business Logic from Delivery Logic: Your core business apps should simply request a notification delivery without worrying about how Telegram authenticates or how WhatsApp formats text.
  2. Support Dynamic Channel Selection: Deliver via a user's preferred channel, or automatically cascade/fallback (e.g., try WhatsApp first; if it fails, send an Email).
  3. Handle Channel Constraints: Translate a single unified notification schema into platform-specific formats (Markdown for Telegram, HTML for Email, strict templates for WhatsApp).
  4. Scale Asynchronously: Use message queues to prevent third-party API latency from slowing down main application threads.

Here is the high-level architecture diagram of the system we're building:

+-------------------+
|  Application Logic|
+---------+---------+
          |
          v
+-------------------+      +-------------------+
| Notification      | ---> | Redis / BullMQ    |
| Dispatcher        |      | Job Queue         |
+-------------------+      +---------+---------+
                                     |
                                     v
                           +-------------------+
                           | Notification      |
                           | Worker Engine     |
                           +---------+---------+
                                     |
         +---------------------------+---------------------------+
         |                           |                           |
         v                           v                           v
+-----------------+         +-----------------+         +-----------------+
| Email Adapter   |         | Telegram Adapter|         | WhatsApp Adapter|
| (Resend/SendGrid|         | (Bot API)       |         | (Meta Cloud API)|
+-----------------+         +-----------------+         +-----------------+

Step 1: Defining Core Abstractions & Payload Contracts

To ensure complete decoupling, we define standard data structures and a clean contract for all notification channels using TypeScript interfaces.

typescript
// types.ts export type ChannelType = 'email' | 'telegram' | 'whatsapp'; export interface NotificationPayload { title: string; body: string; actionUrl?: string; metadata?: Record<string, unknown>; } export interface RecipientProfile { email?: string; telegramChatId?: string; phoneNumber?: string; // E.164 format for WhatsApp, e.g., +14155552671 } export interface DeliveryResult { success: boolean; channel: ChannelType; messageId?: string; error?: string; } // The core adapter interface every provider must implement export interface INotificationChannel { readonly channelType: ChannelType; send(recipient: RecipientProfile, payload: NotificationPayload): Promise<DeliveryResult>; }

Step 2: Implementing Channel Adapters (Strategy Pattern)

Now we implement specific concrete classes for each channel. Each adapter encapsulates the platform's API quirks, formatting requirements, and error handling.

1. The Email Adapter (using standard HTTP / REST)

For email, we'll format the payload into clean HTML.

typescript
// adapters/EmailChannel.ts import { INotificationChannel, ChannelType, RecipientProfile, NotificationPayload, DeliveryResult } from '../types'; export class EmailChannel implements INotificationChannel { readonly channelType: ChannelType = 'email'; constructor(private apiKey: string, private senderEmail: string) {} async send(recipient: RecipientProfile, payload: NotificationPayload): Promise<DeliveryResult> { if (!recipient.email) { return { success: false, channel: this.channelType, error: 'Recipient email missing.' }; } const htmlBody = ` <div style="font-family: sans-serif; padding: 20px;"> <h2>${payload.title}</h2> <p>${payload.body}</p> ${payload.actionUrl ? `<a href="${payload.actionUrl}" style="background: #0070f3; color: white; padding: 10px 15px; text-decoration: none; border-radius: 5px;">View Details</a>` : ''} </div> `; try { // Example implementation using Resend / API call const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ from: this.senderEmail, to: recipient.email, subject: payload.title, html: htmlBody }) }); const data = await response.json(); if (!response.ok) throw new Error(data.message || 'Failed to send email'); return { success: true, channel: this.channelType, messageId: data.id }; } catch (err: any) { return { success: false, channel: this.channelType, error: err.message }; } } }

2. The Telegram Adapter

Telegram's Bot API is remarkably straight-forward, but requires MarkdownV2 or HTML escaping for text styling.

typescript
// adapters/TelegramChannel.ts import { INotificationChannel, ChannelType, RecipientProfile, NotificationPayload, DeliveryResult } from '../types'; export class TelegramChannel implements INotificationChannel { readonly channelType:
M

Written by Miraz Ahmed

Full-stack developer and UI designer crafting beautiful digital experiences. Specializing in React, Next.js, and modern web technologies.

Share