from abc import ABC, abstractmethod
class UnknownChannel(Exception):
pass
class NotificationChannel(ABC):
@abstractmethod
def send(self, recipient, subject, body):
...
_REGISTRY = {}
def register_channel(key):
def decorator(cls):
_REGISTRY[key] = cls
return cls
return decorator
def get_channel(key):
try:
return _REGISTRY[key]()
except KeyError:
raise UnknownChannel("no channel registered for %r" % key)
@register_channel("email")
class EmailChannel(NotificationChannel):
def __init__(self, transport=None):
self._transport = transport or _default_smtp()
def send(self, recipient, subject, body):
msg = {"To": recipient, "Subject": subject, "Body": body}
self._transport.deliver(msg)
return {"channel": "email", "to": recipient}
@register_channel("sms")
class SmsChannel(NotificationChannel):
def send(self, recipient, subject, body):
text = body if not subject else "%s: %s" % (subject, body)
return {"channel": "sms", "to": recipient, "segments": len(text) // 160 + 1}
def _default_smtp():
from smtp_transport import SmtpTransport
return SmtpTransport()
from dataclasses import dataclass
class TemplateError(Exception):
pass
@dataclass(frozen=True)
class RenderedMessage:
subject: str
body: str
class TemplateRegistry:
def __init__(self):
self._templates = {}
def register(self, name, renderer):
self._templates[name] = renderer
return renderer
def render(self, name, context):
if name not in self._templates:
raise TemplateError("unknown template %r" % name)
try:
return self._templates[name](context)
except KeyError as exc:
raise TemplateError("missing context key %s" % exc)
templates = TemplateRegistry()
@templates.register("password_reset")
def _password_reset(ctx):
return RenderedMessage(
subject="Reset your password",
body="Hi %s, use this link: %s" % (ctx["name"], ctx["reset_url"]),
)
@templates.register("order_shipped")
def _order_shipped(ctx):
return RenderedMessage(
subject="Order #%s shipped" % ctx["order_id"],
body="Your order is on its way, tracking: %s" % ctx["tracking"],
)
from channels import get_channel
from templates import templates
class Notifier:
def __init__(self, template_registry=None, channel_factory=get_channel):
self._templates = template_registry or templates
self._channel_factory = channel_factory
def notify(self, channel, template, recipient, context=None):
message = self._templates.render(template, context or {})
transport = self._channel_factory(channel)
return transport.send(recipient, message.subject, message.body)
if __name__ == "__main__":
notifier = Notifier()
receipt = notifier.notify(
channel="email",
template="password_reset",
recipient="user@example.com",
context={"name": "Dana", "reset_url": "https://app/reset/abc"},
)
print(receipt)
sms_receipt = notifier.notify(
channel="sms",
template="order_shipped",
recipient="+15550001111",
context={"order_id": 4821, "tracking": "1Z999"},
)
print(sms_receipt)
This snippet shows how a notification system stays decoupled from the transports that actually deliver messages, using a small pluggable-channel abstraction. The core idea is the strategy pattern combined with a self-registering registry: callers ask to send a named template over a named channel, and neither the caller nor the template layer knows anything concrete about SMTP, an SMS gateway, or a webhook.
In channels.py, NotificationChannel is an abstract base class defining the single method every transport must implement, send(recipient, subject, body). A @register_channel decorator populates a module-level _REGISTRY, so each concrete channel — EmailChannel, SmsChannel — advertises itself under a key without any central wiring. The get_channel factory resolves that key at runtime and raises a clear UnknownChannel error if a caller requests something unregistered, which keeps configuration mistakes loud rather than silent. New transports are added simply by writing a class and decorating it; nothing else changes.
The RenderedMessage dataclass in templates.py carries an already-rendered subject and body so the channel layer never touches templating logic. The TemplateRegistry maps a template name to a callable that turns a context dict into a RenderedMessage. This separation matters: rendering can fail on missing context keys, and render surfaces those as a TemplateError before any transport is invoked, so a half-formed message is never delivered.
In notifier.py, Notifier is the thin orchestration layer that ties the two registries together. Its notify method renders the template first, then looks up the channel and delegates delivery. Because Notifier depends only on the abstractions, it can be unit-tested with a fake channel, and production code can swap SMS for a push provider by changing a string. The trade-off is a little indirection and the need to keep the registry populated at import time, but in return each concern — rendering, transport, orchestration — evolves independently. This pattern is worth reaching for whenever the set of delivery mechanisms is expected to grow or vary per environment.
Related snips
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
default from: 'noreply@example.com'
def welcome_email(user)
@user = user
ActionMailer advanced patterns for transactional emails
module EmailNormalization
extend ActiveSupport::Concern
included do
attr_accessor :soft_warnings
Soft Validation: Normalize + Validate Email
package com.example.myapp.utils
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.app.PendingIntent
Notification channels and categories
class UserRegistrationService
class Result
attr_reader :user, :errors
def initialize(success:, user: nil, errors: [])
@success = success
Service objects for business logic encapsulation
<?php
namespace App\Notifications;
use App\Models\Post;
use Illuminate\Bus\Queueable;
Laravel notifications for multi-channel messaging
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["container"]
connect() {
Toast notifications with Stimulus and Tailwind
Share this code
Here's the card — post it anywhere.