Skip to content

// Design Patterns · 07

Bridge

Two dimensions of changeleads toSplit them, link by reference

Split an abstraction from its implementation so both can vary.

How it fits together

Bridge structure«client»AlertServiceRaise(incident)UrgentAlert«abstract»AlertSend(incident)# channelDigestAlertEmailChannel«interface»MessageChannelDeliver(to, text)SmsChannelSend()bridge
uses / callswraps / holdsextendsimplements
  • AlertService uses / calls Alert (Send())
  • Alert wraps / holds MessageChannel (bridge)
  • UrgentAlert extends Alert
  • DigestAlert extends Alert
  • EmailChannel implements MessageChannel
  • SmsChannel implements MessageChannel

Spot it

  • Class names like UrgentEmailAlert, DigestSmsAlert
  • Each new channel multiplies the class count
  • Two hierarchies that must change separately

It costs

More moving parts up front; pointless if only one side ever varies.

In code

public interface IMessageChannel{    Task DeliverAsync(string to, string text, CancellationToken ct);}​public sealed class EmailChannel(IEmailSender email) : IMessageChannel{    public Task DeliverAsync(string to, string text, CancellationToken ct) =>        email.SendAsync(to, "Alert", text, ct);}​public abstract class Alert(IMessageChannel channel){    protected IMessageChannel Channel { get; } = channel;    public abstract Task SendAsync(Incident incident, CancellationToken ct);}​public sealed class UrgentAlert(IMessageChannel channel) : Alert(channel){    public override Task SendAsync(Incident incident, CancellationToken ct) =>        Channel.DeliverAsync(incident.OnCall, $"URGENT: {incident.Title}", ct);}
Go deeperThe full explanation and what to ask for · 1 min readExpand

Bridge splits one hierarchy that varies in two directions into two hierarchies joined by a reference. The abstraction (what an alert means) holds an implementor (how a message is delivered), and each side grows without touching the other: M kinds plus N channels instead of M × N classes.

Do not build it speculatively; if only one dimension really varies, a single interface is enough. Keep the implementor interface primitive. If it starts mirroring the abstraction method for method, the split is in the wrong place.

Bridge is designed up front so two sides can evolve; Adapter is retrofitted to make existing, incompatible interfaces fit. Structurally the reference is a Strategy held by a whole hierarchy, and the pattern is composition over inheritance at the scale of a design.

Ask for it

  • abstraction
  • implementor
  • refined abstraction
  • class explosion
  • composition over inheritance

We have UrgentEmailAlert, UrgentSmsAlert, DigestEmailAlert and DigestSmsAlert. Split them into an Alert abstraction (Urgent, Digest) that holds a MessageChannel (Email, Sms, Push) passed in its constructor, so a new channel is one class. Keep every existing test passing.