Stop 22 · Build Backend Portfolio ProjectsProjects · 11/14

Notification Service (Email/SMS/Push)

Part of the Backend Developer Roadmap

In short

"Your order has shipped" can go by email, SMS and push. A notification service handles templates, preferences, retries and provider outages, so no other service has to.

A unified service for sending notifications across multiple channels — practice for third-party integrations, templating, and queue-based delivery.

The purple blob holds a megaphone and posts an envelope while a phone rings nearby.

Think of it like this

The college announcement desk

One notice from the principal goes out three ways: the loudspeaker, the notice board and the class WhatsApp group. Departments just hand the notice to the desk. The desk knows who wants which channel and retries if the speaker fails.

Watch it happen

From event to delivered

Event
Preferences
Template
Queue
Provider
Status

1/6The order service emits order.shipped with the order ID and user ID. That's all it does.

The big ideas

What you need to know

One interface, many channels

Email, SMS and push all implement the same send() shape, so adding WhatsApp later is one new class.

Respect preferences

Per-user, per-channel opt-ins and quiet hours. Transactional messages like OTPs may follow different rules from marketing.

Retries and fallback

Providers go down. Queue every send, retry with backoff, and fail over to a second provider if needed.

No double sends

A deterministic job ID per event, user and channel means a retried event doesn't spam the user.

See it in code

Walk through the code

ts
1interface Channel {2  name: "email" | "sms" | "push";3  send(to: string, message: { title: string; body: string }): Promise<{ providerId: string }>;4}5 6async function notify(userId: string, event: string, data: Record<string, string>) {7  const prefs = await getPreferences(userId);  // e.g. { email: true, sms: false, push: true }8  const message = renderTemplate(event, data); // "Your order #42 has shipped"9 10  for (const channel of channels.filter((c) => prefs[c.name])) {11    await queue.add("deliver", { userId, channel: channel.name, message }, {12      jobId: `${event}:${data.orderId}:${userId}:${channel.name}`, // same event twice = one job13      attempts: 5, backoff: { type: "exponential", delay: 2000 },14    });15  }16}

1/3Every channel exposes the same send() method.

queue here is a BullMQ Queue. A job with an ID that already exists isn't added again, which is what stops duplicate sends.

Save yourself the pain

Common mistakes

Oops

Calling the email provider directly inside the order API request.

Do this instead

If the provider is slow, checkout is slow. Emit an event and let the notification service queue it.

Oops

Retrying a failed SMS with no deduplication.

Do this instead

Give every send a stable ID. Otherwise a timeout that actually succeeded sends the OTP twice.

Your mission

Milestone 1: email and push from one event

~45 min

Quick check

Why give each notification job an ID built from the event, user and channel?

Go deeper

  1. 1Integrate at least two channels, e.g. email via SendGrid and SMS via Twilio.
  2. 2Process sends through a queue so a slow provider doesn't block your main API.
  3. 3Add delivery status tracking and retry logic for failed sends.

More projects at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge