Bridge Pattern — FixIt Pro Series #07
FixIt Pro needs to send urgent, high, and normal notifications across SMS, email, and push channels. Learn how the Bridge pattern prevents a class explosion by separating urgency levels from notification channels — in C# and TypeScript.
Series: Design Patterns with FixIt Pro · Episode 07 / 22 · Structural Pattern
Previous: #06 — Adapter Pattern
The Scenario
FixIt Pro's notification system has two dimensions that keep growing independently:
Urgency levels — how the message is worded and how aggressively it's sent:
- Normal — polite update, sent once
- High — firm reminder, sent with follow-up
- Urgent — immediate alert, sent repeatedly until acknowledged
Notification channels — where the message is delivered:
- SMS
- Push notification
If you model this with inheritance, you end up needing a class for every combination:
NormalEmailNotification
NormalSmsNotification
NormalPushNotification
HighEmailNotification
HighSmsNotification
HighPushNotification
UrgentEmailNotification
UrgentSmsNotification
UrgentPushNotification
That's 9 classes for 3 urgency levels × 3 channels. Add a fourth channel (WhatsApp) and a fourth urgency level (Critical) and you're at 16 classes. This explosion grows multiplicatively — it's unsustainable.
The Bridge pattern cuts this down to 3 + 3 = 6 classes by separating the two dimensions.
What Is the Bridge Pattern?
Decouple an abstraction from its implementation so that the two can vary independently.
The key word is decouple. Instead of baking the channel into the urgency class via inheritance, you give the urgency class a reference to a channel — and that reference can be swapped at runtime.
The "bridge" is that reference — a composition link between two independent hierarchies.
The four participants
| Role | FixIt Pro equivalent |
|---|---|
| Abstraction | JobNotification — defines urgency-level behaviour |
| Refined Abstractions | NormalNotification, HighNotification, UrgentNotification |
| Implementor interface | INotificationChannel — defines how a channel sends |
| Concrete Implementors | EmailChannel, SmsChannel, PushChannel |
The Abstraction holds a reference to the Implementor. The two hierarchies grow independently — add a new channel without touching urgency classes; add a new urgency level without touching channel classes.
C# Implementation
// ── Implementor Interface ──────────────────────────────────
public interface INotificationChannel
{
void Send(string recipient, string subject, string body);
}
// ── Concrete Implementors ──────────────────────────────────
public class EmailChannel : INotificationChannel
{
public void Send(string recipient, string subject, string body) =>
Console.WriteLine($"📧 Email → {recipient} | {subject}: {body}");
}
public class SmsChannel : INotificationChannel
{
public void Send(string recipient, string subject, string body) =>
Console.WriteLine($"📱 SMS → {recipient}: {subject} — {body}");
}
public class PushChannel : INotificationChannel
{
public void Send(string recipient, string subject, string body) =>
Console.WriteLine($"🔔 Push → {recipient} | {subject}: {body}");
}
public class WhatsAppChannel : INotificationChannel
{
public void Send(string recipient, string subject, string body) =>
Console.WriteLine($"💬 WA → {recipient}: *{subject}* {body}");
}
// ── Abstraction ────────────────────────────────────────────
public abstract class JobNotification
{
// The bridge — a reference to the implementor
protected readonly INotificationChannel Channel;
protected JobNotification(INotificationChannel channel)
{
Channel = channel;
}
public abstract void Notify(string jobId, string recipient, string message);
}
// ── Refined Abstractions ───────────────────────────────────
public class NormalNotification : JobNotification
{
public NormalNotification(INotificationChannel channel) : base(channel) { }
public override void Notify(string jobId, string recipient, string message)
{
Channel.Send(
recipient,
$"Job #{jobId} Update",
message
);
}
}
public class HighNotification : JobNotification
{
public HighNotification(INotificationChannel channel) : base(channel) { }
public override void Notify(string jobId, string recipient, string message)
{
Channel.Send(
recipient,
$"⚠️ Action Required — Job #{jobId}",
$"{message} Please respond promptly."
);
}
}
public class UrgentNotification : JobNotification
{
private readonly int _repeatCount;
public UrgentNotification(INotificationChannel channel, int repeatCount = 3)
: base(channel)
{
_repeatCount = repeatCount;
}
public override void Notify(string jobId, string recipient, string message)
{
for (int i = 0; i < _repeatCount; i++)
{
Channel.Send(
recipient,
$"🚨 URGENT — Job #{jobId} [{i + 1}/{_repeatCount}]",
$"{message} Immediate response required."
);
}
}
}
// ── Client Code ────────────────────────────────────────────
class Program
{
static void Main()
{
// Mix and match any urgency with any channel
Console.WriteLine("=== Normal job update via Email ===");
var normalEmail = new NormalNotification(new EmailChannel());
normalEmail.Notify("JC-201", "[email protected]", "Your handyman has been assigned.");
Console.WriteLine("\n=== High priority via SMS ===");
var highSms = new HighNotification(new SmsChannel());
highSms.Notify("JC-202", "+264-81-000-1111", "Handyman running 30 minutes late.");
Console.WriteLine("\n=== Urgent via Push (3 repeats) ===");
var urgentPush = new UrgentNotification(new PushChannel(), repeatCount: 3);
urgentPush.Notify("JC-203", "device-token-xyz", "Emergency: no water in the building.");
Console.WriteLine("\n=== Urgent via WhatsApp — new channel, zero changes to urgency classes ===");
var urgentWhatsApp = new UrgentNotification(new WhatsAppChannel(), repeatCount: 2);
urgentWhatsApp.Notify("JC-204", "+264-81-000-2222", "Emergency: power outage in Block A.");
}
}
Output:
=== Normal job update via Email ===
📧 Email → [email protected] | Job #JC-201 Update: Your handyman has been assigned.
=== High priority via SMS ===
📱 SMS → +264-81-000-1111: ⚠️ Action Required — Job #JC-202 — Handyman running 30 minutes late. Please respond promptly.
=== Urgent via Push (3 repeats) ===
🔔 Push → device-token-xyz | 🚨 URGENT — Job #JC-203 [1/3]: Emergency: no water in the building. Immediate response required.
🔔 Push → device-token-xyz | 🚨 URGENT — Job #JC-203 [2/3]: Emergency: no water in the building. Immediate response required.
🔔 Push → device-token-xyz | 🚨 URGENT — Job #JC-203 [3/3]: Emergency: no water in the building. Immediate response required.
=== Urgent via WhatsApp — new channel, zero changes to urgency classes ===
💬 WA → +264-81-000-2222: *🚨 URGENT — Job #JC-204 [1/2]* Emergency: power outage in Block A. Immediate response required.
💬 WA → +264-81-000-2222: *🚨 URGENT — Job #JC-204 [2/2]* Emergency: power outage in Block A. Immediate response required.
TypeScript Implementation
// ── Channel/INotificationChannel.ts ───────────────────────
export interface INotificationChannel {
send(recipient: string, subject: string, body: string): void;
}
// ── Channel/EmailChannel.ts ────────────────────────────────
import { INotificationChannel } from "./INotificationChannel";
export class EmailChannel implements INotificationChannel {
send(recipient: string, subject: string, body: string): void {
console.log(`📧 Email → ${recipient} | ${subject}: ${body}`);
}
}
// ── Channel/SmsChannel.ts ──────────────────────────────────
import { INotificationChannel } from "./INotificationChannel";
export class SmsChannel implements INotificationChannel {
send(recipient: string, subject: string, body: string): void {
console.log(`📱 SMS → ${recipient}: ${subject} — ${body}`);
}
}
// ── Channel/PushChannel.ts ─────────────────────────────────
import { INotificationChannel } from "./INotificationChannel";
export class PushChannel implements INotificationChannel {
send(recipient: string, subject: string, body: string): void {
console.log(`🔔 Push → ${recipient} | ${subject}: ${body}`);
}
}
// ── Channel/WhatsAppChannel.ts ─────────────────────────────
import { INotificationChannel } from "./INotificationChannel";
export class WhatsAppChannel implements INotificationChannel {
send(recipient: string, subject: string, body: string): void {
console.log(`💬 WA → ${recipient}: *${subject}* ${body}`);
}
}
// ── Notification/JobNotification.ts ───────────────────────
import { INotificationChannel } from "../Channel/INotificationChannel";
export abstract class JobNotification {
// The bridge — composition reference to the implementor
constructor(protected readonly channel: INotificationChannel) {}
abstract notify(jobId: string, recipient: string, message: string): void;
}
// ── Notification/NormalNotification.ts ────────────────────
import { JobNotification } from "./JobNotification";
import { INotificationChannel } from "../Channel/INotificationChannel";
export class NormalNotification extends JobNotification {
constructor(channel: INotificationChannel) { super(channel); }
notify(jobId: string, recipient: string, message: string): void {
this.channel.send(
recipient,
`Job #${jobId} Update`,
message
);
}
}
// ── Notification/HighNotification.ts ──────────────────────
import { JobNotification } from "./JobNotification";
import { INotificationChannel } from "../Channel/INotificationChannel";
export class HighNotification extends JobNotification {
constructor(channel: INotificationChannel) { super(channel); }
notify(jobId: string, recipient: string, message: string): void {
this.channel.send(
recipient,
`⚠️ Action Required — Job #${jobId}`,
`${message} Please respond promptly.`
);
}
}
// ── Notification/UrgentNotification.ts ────────────────────
import { JobNotification } from "./JobNotification";
import { INotificationChannel } from "../Channel/INotificationChannel";
export class UrgentNotification extends JobNotification {
constructor(
channel: INotificationChannel,
private readonly repeatCount: number = 3
) {
super(channel);
}
notify(jobId: string, recipient: string, message: string): void {
for (let i = 0; i < this.repeatCount; i++) {
this.channel.send(
recipient,
`🚨 URGENT — Job #${jobId} [${i + 1}/${this.repeatCount}]`,
`${message} Immediate response required.`
);
}
}
}
// ── App.ts ─────────────────────────────────────────────────
import { EmailChannel } from "./Channel/EmailChannel";
import { SmsChannel } from "./Channel/SmsChannel";
import { PushChannel } from "./Channel/PushChannel";
import { WhatsAppChannel } from "./Channel/WhatsAppChannel";
import { NormalNotification } from "./Notification/NormalNotification";
import { HighNotification } from "./Notification/HighNotification";
import { UrgentNotification } from "./Notification/UrgentNotification";
console.log("=== Normal job update via Email ===");
const normalEmail = new NormalNotification(new EmailChannel());
normalEmail.notify("JC-201", "[email protected]", "Your handyman has been assigned.");
console.log("\n=== High priority via SMS ===");
const highSms = new HighNotification(new SmsChannel());
highSms.notify("JC-202", "+264-81-000-1111", "Handyman running 30 minutes late.");
console.log("\n=== Urgent via Push (3 repeats) ===");
const urgentPush = new UrgentNotification(new PushChannel(), 3);
urgentPush.notify("JC-203", "device-token-xyz", "Emergency: no water in the building.");
console.log("\n=== Urgent via WhatsApp — new channel, zero changes to notification classes ===");
const urgentWhatsApp = new UrgentNotification(new WhatsAppChannel(), 2);
urgentWhatsApp.notify("JC-204", "+264-81-000-2222", "Emergency: power outage in Block A.");
C# vs TypeScript — Key Differences
| Aspect | C# | TypeScript |
|---|---|---|
| Abstract class | abstract class + abstract methods |
abstract class + abstract methods (identical) |
| Protected field | protected readonly INotificationChannel Channel |
protected readonly channel: INotificationChannel in constructor |
| Constructor chaining | : base(channel) |
super(channel) |
| Default parameter | int repeatCount = 3 |
repeatCount: number = 3 |
| Loop | for (int i = 0; ...) |
for (let i = 0; ...) |
The Bridge pattern translates almost identically between the two languages. The structural concept — abstraction holds a reference to implementor — maps cleanly to both type systems.
Bridge vs Adapter — What's the Difference?
This is one of the most common sources of confusion in design patterns.
| Adapter | Bridge | |
|---|---|---|
| Intent | Make two incompatible things work together | Prevent two dimensions from being coupled together |
| When designed | After the fact — existing classes need connecting | Upfront — new system designed with flexibility in mind |
| Direction | One-way translation | Two-way independent variation |
| FixIt Pro | Legacy SMS gateway → INotifier | Urgency levels ↔ Notification channels |
The Adapter is a retrofit. The Bridge is a design decision. If you find yourself reaching for the Adapter pattern on new code you're writing from scratch, consider whether the Bridge is actually what you need.
When to Use the Bridge
Use it when:
- You want to avoid a permanent binding between abstraction and implementation
- Both abstraction and implementation should be extensible via subclassing
- You have a class explosion from combining two independent dimensions
- You want to switch implementations at runtime
Avoid it when:
- You only have one implementation — the extra abstraction layer adds complexity without benefit
- The two dimensions don't actually vary independently — if they're always paired, composition without the abstract hierarchy is simpler
Real-World Takeaway
The Bridge pattern is everywhere in UI frameworks. In Android, View (abstraction) is separated from the rendering implementation — you can swap between hardware-accelerated and software rendering without changing the view hierarchy. In .NET, DbConnection (abstraction) bridges to provider-specific implementations (SqlConnection, NpgsqlConnection, SQLiteConnection) — your data access code doesn't change when you swap databases.
In FixIt Pro, adding WhatsApp as a fourth channel required exactly one new class — WhatsAppChannel. The three urgency classes were completely untouched. That's the Bridge delivering on its promise: independent variation on both sides.
Repo Structure for This Episode
github.com/antonlungameni/fixit-pro-design-patterns
fixit-pro-design-patterns/
├── csharp/Structural/07-Bridge/
│ ├── Channel/INotificationChannel.cs
│ ├── Channel/EmailChannel.cs
│ ├── Channel/SmsChannel.cs
│ ├── Channel/PushChannel.cs
│ ├── Channel/WhatsAppChannel.cs
│ ├── Notification/JobNotification.cs
│ ├── Notification/NormalNotification.cs
│ ├── Notification/HighNotification.cs
│ ├── Notification/UrgentNotification.cs
│ └── Program.cs
└── typescript/Structural/07-Bridge/
├── Channel/INotificationChannel.ts
├── Channel/EmailChannel.ts
├── Channel/SmsChannel.ts
├── Channel/PushChannel.ts
├── Channel/WhatsAppChannel.ts
├── Notification/JobNotification.ts
├── Notification/NormalNotification.ts
├── Notification/HighNotification.ts
├── Notification/UrgentNotification.ts
└── App.ts
Previous: #06 — Adapter Pattern
Next up: #08 — Composite Pattern
A compound job card — "Full Kitchen Renovation" — made up of smaller individual job cards. The client treats a single job and a group of jobs identically.