Skip to main content
CleanCodeMastery

Bridge Pattern: One Remote, Many Devices — Stop the Subclass Explosion

Understand the Bridge pattern with a TV and remote story. Split one big class into two parts that grow separately, so you avoid too many extra subclasses.

24 min read Updated June 11, 2026beginner
design-patternsstructural-patternsbridgecompositiontypescriptcsharpoop

📺 The drawer full of remotes

Meera's family in Pune loves gadgets. Meera is thirteen, and she is the official "remote finder" of the house, because nobody else can ever find the right one.

In their living room there is a TV. In the bedroom there is a radio that Aji listens to every morning. Last Diwali, Papa brought home a fancy set-top box too. And with every device came... a remote. The TV remote. The radio remote. The set-top box remote. Then the old TV remote stopped working, so Papa bought a new "advanced" remote with extra buttons. Soon, the drawer below the TV was full of remotes. Every evening, the same drama: "Meera! Which remote is for which machine?"

One Sunday, Meera's uncle Ajay visited from Bengaluru with a gift — a universal remote. One single remote with simple buttons: power, volume up, volume down. On its side was a small switch to select which device it should talk to. Point it at the TV, it controls the TV. Switch it, point it at the radio, it controls the radio. Aji was suspicious for exactly one day, and then she hid all the old remotes in her cupboard "for safety".

Stop and think about why this works. The universal remote does not know how a TV works inside. It does not know how a radio works inside. It only knows a small common language: "turn on", "turn off", "volume up", "volume down". Every device promises to understand this small language. The remote speaks the language; the device does the actual electrical work.

Now the two sides can grow separately:

  • The remote company can launch a basic remote, a kids' remote with a lock button, and a premium remote with a mute button. They never need to open a TV.
  • The device companies can launch new TVs, radios, projectors, and soundbars. They never need to design a remote.

Nobody ever builds a welded product called "AdvancedRemoteForSonyTV". Any remote drives any device, because between them stands a small, agreed interface. That connecting link is the bridge — and this is exactly the Bridge pattern.

Here is Meera's whole Sunday as a journey. Watch the mood change when the universal remote arrives:

Figure 1: Meera's journey from remote chaos to one universal remote

🌉 What is the Bridge pattern?

Here is the definition in plain words.

The Bridge pattern is a structural design pattern that splits one large class (or a tightly related set of classes) into two separate hierarchies — an abstraction (the high-level control side) and an implementation (the low-level working side) — connected by a simple object reference, so that both sides can change and grow independently.

Let us decode the two special words, because they confuse everyone at first:

Pattern roleMeaningIn Meera's story
AbstractionThe side the user talks to; offers friendly, high-level operations; does not do the real work itselfThe universal remote
Refined AbstractionA fancier variant of the abstractionThe premium remote with a mute button
ImplementationThe small agreed interface of low-level operationsThe common language: on, off, set volume
Concrete ImplementationA real worker that fulfils the interface in its own wayThe TV, the radio, the set-top box

The abstraction holds a reference to an implementation object and forwards work across it. That reference — the has-a link — is the bridge. The remote holds a device; it is never a kind of device.

💡

Careful! In the Bridge pattern, "implementation" does not mean "a subclass of the abstraction". The two sides are parallel families, standing side by side like two banks of a river. The remote is never a kind of TV, and the TV is never a kind of remote. They are joined only by the bridge reference. If you remember just this one sentence, you have understood half the pattern.

The full idea on one page:

Figure 2: The Bridge pattern at a glance

💥 The problem it solves

Why do we need this at all? Let us see what happens without Bridge — and this time, let us count the damage properly.

Suppose Meera starts writing remote software with plain inheritance. She makes a class for every remote–device pair:

// BAD: one class per combination. Watch what happens...
class BasicRemoteForTv {
  /* basic buttons + TV wiring code */
}
class BasicRemoteForRadio {
  /* SAME basic buttons + radio wiring code */
}
class AdvancedRemoteForTv {
  /* extra buttons + SAME TV wiring code */
}
class AdvancedRemoteForRadio {
  /* extra buttons + SAME radio wiring code */
}

With 2 remotes and 2 devices, she already has 4 classes — and lots of copied code. Now Papa buys a set-top box. She needs BasicRemoteForSetTopBox and AdvancedRemoteForSetTopBox. Six classes. Then the company designs a kids' remote. Nine classes! Every new remote type multiplies with every device, and every new device multiplies with every remote.

The count is always remotes × devices:

2 remotes × 2 devices = 4 classes
3 remotes × 3 devices = 9 classes
4 remotes × 5 devices = 20 classes!

This runaway growth is called the Cartesian explosion (or class explosion). It happens because we are stuffing two independent dimensions of changewhat kind of remote and what kind of device — into one inheritance tree. Inheritance can model only one dimension cleanly. Force a second one in, and every new value on one axis multiplies with every value on the other.

Figure 3: Without Bridge, every new remote or device multiplies the classes

Bridge says: stop multiplying, start adding. Keep remotes in one family, devices in another, and connect them with a reference. Then 3 remotes + 3 devices = 6 classes instead of 9, and 4 remotes + 5 devices = 9 classes instead of 20. The gap between the two approaches grows shockingly fast — look at the bars race away from the line:

Figure 4: Classes needed as combos grow — inheritance multiplies, Bridge adds

College corner: this is the Cartesian product from discrete maths wearing a software costume. If set R holds the remote types and set D holds the device types, inheritance forces you to write one class per element of R × D, which has size m × n. Bridge replaces the product with a disjoint union: you write the m abstraction classes plus the n implementation classes, total m + n. In complexity language, the class count drops from O(m·n) to O(m + n). The trick generalises: with three independent dimensions (say message type × channel × language), naive inheritance needs m·n·k classes, while composition needs m + n + k. Whenever you see multiplication in a design, look for a way to turn it into addition — that instinct is worth more than memorising any single pattern.

There is a second pain hiding here too: duplication. The TV-controlling code inside BasicRemoteForTv and AdvancedRemoteForTv is identical. Fix a bug in one, and you must remember to fix the other. That is how bugs survive. In a typical exploded family, most of the code is copies:

Figure 5: What fills the exploded classes

Only about a quarter of the code is genuinely new in each class. Bridge keeps exactly that quarter and deletes the rest.

🛠️ How it works, step by step

Here is the recipe for building a Bridge, step by step.

  1. Spot the two independent dimensions. Ask: "What are the two different reasons this class family keeps growing?" In our story: remote features grow, and device types grow. Common pairs in real projects: app logic vs operating system, GUI vs platform, notification type vs delivery channel.
  2. Choose which side is the Implementation. Pick the lower-level, more "hardware-ish" side. Here, devices. The other side (remotes) becomes the Abstraction.
  3. Write the Implementation interface. Keep it small and primitive: enable(), disable(), getVolume(), setVolume(). The abstraction will combine these small bricks into bigger behaviour.
  4. Write the Abstraction class with a field of the Implementation type. Every high-level method works only through that field.
  5. Add Refined Abstractions — subclasses of the abstraction for fancier variants (e.g., an advanced remote with mute()).
  6. Write Concrete Implementations — one class per device, each fulfilling the interface in its own way.
  7. Wire them in the client: create a device, hand it to a remote's constructor, and use the remote.
Figure 6: Bridge structure — two parallel families joined by one reference

See the shape of the diagram? Two towers, one thin link. The left tower (remotes) can grow downward with new remote types. The right tower (devices) can grow downward with new device types. The only thing both must respect is the small Device interface in the middle.

Now follow one button press across the bridge. Meera presses "volume up" on the remote, and the work crosses the reference into whatever device is connected:

Figure 7: One button press crossing the bridge

The remote never says "TV". It says getVolume() and setVolume() to whatever sits behind the interface. Replace the TV with a radio, and this diagram does not change a single arrow on the remote's side.

The device itself lives a simple life, which we can draw as states. The remote's buttons are just events that move the device between these states:

Figure 8: A device's life, driven from across the bridge

Notice something lovely: this state chart is true for the TV, the radio, and any future projector. The states belong to the implementation side, the button presses come from the abstraction side, and the bridge carries the events across.

💻 Real-life code example

Let us code Meera's living room in TypeScript. Watch carefully how the two dimensions stay in two separate places, and how any remote can drive any device.

// =======================================================
// DIMENSION 1: the IMPLEMENTATION side (devices)
// Small, primitive operations only.
// =======================================================
interface Device {
  isEnabled(): boolean;
  enable(): void;
  disable(): void;
  getVolume(): number;
  setVolume(percent: number): void;
  getName(): string;
}
 
class Tv implements Device {
  private on = false;
  private volume = 30;
 
  isEnabled() { return this.on; }
  enable() { this.on = true; console.log("TV: screen lights up"); }
  disable() { this.on = false; console.log("TV: screen goes dark"); }
  getVolume() { return this.volume; }
  setVolume(percent: number) {
    this.volume = Math.max(0, Math.min(100, percent));
    console.log(`TV: volume is now ${this.volume}`);
  }
  getName() { return "TV"; }
}
 
class Radio implements Device {
  private on = false;
  private volume = 50;
 
  isEnabled() { return this.on; }
  enable() { this.on = true; console.log("Radio: crackles to life"); }
  disable() { this.on = false; console.log("Radio: goes silent"); }
  getVolume() { return this.volume; }
  setVolume(percent: number) {
    this.volume = Math.max(0, Math.min(100, percent));
    console.log(`Radio: volume is now ${this.volume}`);
  }
  getName() { return "Radio"; }
}
 
// =======================================================
// DIMENSION 2: the ABSTRACTION side (remotes)
// High-level buttons, built from the device primitives.
// =======================================================
class RemoteControl {
  // THE BRIDGE: the remote holds a device by reference.
  constructor(protected device: Device) {}
 
  togglePower(): void {
    if (this.device.isEnabled()) this.device.disable();
    else this.device.enable();
  }
 
  volumeUp(): void {
    this.device.setVolume(this.device.getVolume() + 10);
  }
 
  volumeDown(): void {
    this.device.setVolume(this.device.getVolume() - 10);
  }
}
 
// A refined abstraction: extra feature, but it still talks
// ONLY through the Device interface. It has no idea whether
// a TV or a radio is on the other side of the bridge.
class AdvancedRemote extends RemoteControl {
  mute(): void {
    console.log(`AdvancedRemote: muting the ${this.device.getName()}`);
    this.device.setVolume(0);
  }
}
 
// =======================================================
// CLIENT: mix ANY remote with ANY device.
// =======================================================
const tv = new Tv();
const basicRemote = new RemoteControl(tv);
basicRemote.togglePower();
basicRemote.volumeUp();
 
const radio = new Radio();
const smartRemote = new AdvancedRemote(radio);
smartRemote.togglePower();
smartRemote.mute();
 
// Same advanced remote class, pointed at the TV instead:
const smartTvRemote = new AdvancedRemote(tv);
smartTvRemote.mute();
 
// Output:
// TV: screen lights up
// TV: volume is now 40
// Radio: crackles to life
// AdvancedRemote: muting the Radio
// Radio: volume is now 0
// AdvancedRemote: muting the TV
// TV: volume is now 0

Count the classes: 2 remotes + 2 devices = 4 classes, and we got all 4 combinations free of cost. With the inheritance approach we would have needed 4 combination classes already, and 9 at the next growth step.

Notice three beautiful things:

  1. AdvancedRemote.mute() is written once and instantly works for every device — present and future. Tomorrow's Projector class gets mute support the moment it implements Device.
  2. The remote can be re-pointed at runtime — we created AdvancedRemote once for the radio and once for the TV. You could even add a setDevice() method and switch devices while the program runs, exactly like the little switch on uncle Ajay's universal remote.
  3. Each side can be tested alone. Give the remote a fake device, and you can test all its buttons without any real TV code.

🌐 The same idea in C# and Python

Here is the same living room, shortened, in C#:

// Implementation side
public interface IDevice
{
    bool IsEnabled { get; }
    void Enable();
    void Disable();
    int Volume { get; set; }
}
 
public class Tv : IDevice
{
    public bool IsEnabled { get; private set; }
    public int Volume { get; set; } = 30;
    public void Enable() { IsEnabled = true; Console.WriteLine("TV on"); }
    public void Disable() { IsEnabled = false; Console.WriteLine("TV off"); }
}
 
// Abstraction side — holds the bridge reference
public class RemoteControl
{
    protected readonly IDevice Device;
    public RemoteControl(IDevice device) => Device = device;
 
    public void TogglePower()
    {
        if (Device.IsEnabled) Device.Disable();
        else Device.Enable();
    }
}
 
public class AdvancedRemote : RemoteControl
{
    public AdvancedRemote(IDevice device) : base(device) { }
    public void Mute() { Device.Volume = 0; Console.WriteLine("Muted"); }
}
 
// Client
var remote = new AdvancedRemote(new Tv());
remote.TogglePower();  // Output: TV on
remote.Mute();         // Output: Muted

And to show the pattern outside the living room, here is the famous notification system version in Python — message types on the abstraction side, delivery channels on the implementation side. This exact design appears in countless real backends:

# Implementation side: delivery channels (the bricks)
class EmailChannel:
    def deliver(self, text: str) -> None:
        print(f"EMAIL: {text}")
 
class SmsChannel:
    def deliver(self, text: str) -> None:
        print(f"SMS: {text}")
 
# Abstraction side: notification types (the walls)
class Notification:
    def __init__(self, channel):
        self._channel = channel  # THE BRIDGE
 
    def send(self, text: str) -> None:
        self._channel.deliver(text)
 
class UrgentNotification(Notification):
    def send(self, text: str) -> None:
        for _ in range(3):  # urgent means three times!
            self._channel.deliver(f"URGENT: {text}")
 
# Client: any type rides any channel
UrgentNotification(SmsChannel()).send("School closed tomorrow")
Notification(EmailChannel()).send("PTM on Friday")
# Output:
# SMS: URGENT: School closed tomorrow
# SMS: URGENT: School closed tomorrow
# SMS: URGENT: School closed tomorrow
# EMAIL: PTM on Friday

Same picture in every language: the control family and the worker family stand apart, and a single constructor-injected reference joins them.

College corner: C++ programmers meet Bridge under two older names — Handle/Body and Pimpl (pointer to implementation). In Pimpl, a class's header exposes only a thin handle, and a pointer leads to a hidden body class holding all the private members. This cuts compile-time dependencies: changing the body does not force every file that includes the header to recompile. It is the same shape as our remote and device — a stable front, a swappable back, joined by one pointer. Also note how Bridge relates to dependency injection: handing the device into the remote's constructor is constructor injection. DI frameworks industrialise exactly this move, which is why well-designed DI-heavy codebases are quietly full of bridges.

🌍 Where you see it in real software

Bridge is everywhere once you learn to see it.

  • JDBC drivers in Java. Your Java program talks to the high-level java.sql interfaces (the abstraction): open a connection, run a query, read results. Behind the scenes, a JDBC driver for MySQL, PostgreSQL, or Oracle (the implementation) does the database-specific work. Your code does not change when the database changes — you just plug in a different driver. The classic InformIT article "A Classic Example of Bridge: Drivers" explains exactly this, and ADO.NET providers in C# follow the same shape with DbConnection and provider-specific implementations.
  • Device drivers in operating systems. The OS exposes one stable idea of "a printer" or "a disk" (abstraction). Each manufacturer ships a driver (implementation) that fulfils that contract for its specific hardware. New printer models arrive every month, yet Windows itself does not need rewriting — that is two dimensions growing independently.
  • GUI toolkits. Early Java AWT used "peer" classes: the cross-platform component API on one side, and per-operating-system peers doing the real drawing on the other. Modern graphics layers keep the same split between a drawing API and platform back-ends.
  • Notification systems. The interview classic you just coded in Python: message types (alert, reminder, promotion) on the abstraction side, delivery channels (email, SMS, WhatsApp, push) on the implementation side. Without Bridge: types × channels classes. With Bridge: types + channels.
  • Game engines. A renderer abstraction (draw sprite, draw mesh) bridges to DirectX, Vulkan, or Metal back-ends. The gameplay team and the graphics team work on opposite banks of the river and rarely block each other.
  • Open-source examples to read. The iluwatar/java-design-patterns Bridge example bridges weapons with enchantments (a flying magic hammer, anyone?). Refactoring.Guru's Bridge page walks through the very remote-and-device example you learned today.

✅ When to use it and when not to

Uncle Ajay did not gift a universal remote to his neighbour who owns exactly one fan with one switch. The pattern pays only when both dimensions really vary. Check your situation against this table:

SituationUse Bridge?Why
Your class family grows along two independent directions (shape × colour, remote × device, message × channel)✅ YesThis is the classic trigger — kill the multiplication
You see subclass names like RedCircle, BlueCircle, RedSquare appearing✅ YesCartesian explosion has already started
You want to swap the low-level part at runtime (change database, change renderer)✅ YesThe implementation is a held reference, so it can be replaced live
Two teams must work on the two sides without waiting for each other✅ YesEach team owns one hierarchy behind a stable interface
You want client code to never see platform details✅ YesClients depend only on the abstraction's interface
Your class varies in only ONE direction❌ NoPlain inheritance or a simple strategy is enough — Bridge adds empty ceremony
The "two dimensions" are actually tightly tangled and always change together❌ NoSplitting them creates two files that must always be edited together — worse than one
The project is tiny and will stay tiny❌ NoThe extra interface and indirection cost more than they save

The same decision as a picture — the top-right corner is Bridge country:

Figure 9: Should you build the bridge?

⚠️ Common mistakes students make

⚠️

The most common mistake: pushing high-level logic down into the implementation. The Device interface should hold only small, primitive operations like setVolume(). The moment you add muteAndShowMutedIcon() to the Device interface, every device must copy that combined logic, and the abstraction loses its job. Rule of thumb: implementations offer bricks, abstractions build walls.

More traps to dodge:

  • Thinking abstraction = interface and implementation = class. In Bridge, these words mean "control side" and "work side". The abstraction is usually a normal class (the remote), not a language interface. Do not let the everyday meaning of the words mislead you.
  • Connecting the two sides by inheritance. If your remote extends Tv, you have welded the bridge shut. The connection must be a field (has-a), never a parent (is-a).
  • Creating the bridge but only ever using one pairing. If there is exactly one remote and exactly one device, and no second one is even planned, you have built a bridge across a dry river. Wait until the second dimension really appears.
  • Letting the abstraction peek at concrete devices. Code like if (device instanceof Tv) inside the remote destroys the whole benefit. The abstraction must work only through the interface.
  • Making the implementation interface fat. Twenty methods in Device means every new device must write twenty methods. Keep the brick set small; let remotes combine bricks.

👪 Compare with cousins

Bridge gets confused with Adapter and Strategy more than with anything else. Here is the side-by-side view:

QuestionBridgeAdapterStrategy
When is it applied?Up front, while designingAfter the fact, as a rescueWhile designing, for algorithms
What is the goal?Let two dimensions grow independentlyMake two existing, incompatible interfaces fitSwap an algorithm at runtime
Were the pieces designed to fit?Yes, deliberatelyNo — that is the whole problemYes
How big is the separated side?A whole hierarchy of implementationsUsually one wrapped adapteeA family of interchangeable algorithms
Memory trick"Planned bridge across a river""Travel plug bought in emergency""Choose your route to school"

A short story version: Bridge is planned; Adapter is a rescue. With Bridge, you sit down before building and say, "Remotes and devices will both keep growing — let us separate them now." With Adapter, the charger and socket already exist, already do not fit, and you run to the shop for a fix. Remember Aarav from the previous post? He never planned the forty-rupee adapter. Uncle Ajay's universal remote, on the other hand, was designed from day one to drive many devices.

Strategy deserves a sentence too: its structure (a class delegating to a held interface) looks identical to Bridge. The difference is intent — Strategy swaps one algorithm (e.g., different sorting methods), while Bridge separates an entire implementation dimension. If your "strategy" side starts growing its own rich hierarchy, congratulations, you have discovered a Bridge.

Figure 10: Bridge vs Adapter — planned split vs emergency fix

College corner: the GoF book gives Bridge a one-line summary worth quoting in exams — "decouple an abstraction from its implementation so that the two can vary independently." It is also the cleanest illustration of the design principle "favour composition over inheritance": the welded inheritance design hard-codes the remote–device pairing at compile time, while the composed design defers it to runtime, one constructor argument. Bridges also enable the open/closed principle on two axes at once — you can extend remotes without touching devices and extend devices without touching remotes, modifying neither.

📦 Quick revision box

+=====================================================================+
|                     BRIDGE PATTERN — REVISION CARD                  |
+=====================================================================+
|  Type        : Structural pattern                                   |
|  Nicknames   : Handle/Body, Pimpl (C++)                             |
|  Story       : Universal remote + TV / radio / set-top box          |
|                                                                     |
|  Players     : Abstraction      -> remote (high-level buttons)      |
|                RefinedAbstraction -> advanced remote (mute)         |
|                Implementation   -> Device interface (primitives)    |
|                ConcreteImpl     -> Tv, Radio, SetTopBox             |
|                                                                     |
|  The bridge  : a has-a REFERENCE from remote to device              |
|  Big win     : remotes + devices, NOT remotes x devices             |
|  Maths       : O(m+n) classes instead of O(m*n)                     |
|  Runtime     : implementation can be swapped on a live object       |
|                                                                     |
|  Remember    : Implementations give BRICKS,                         |
|                abstractions build WALLS.                            |
|  vs Adapter  : Bridge is planned; Adapter is a rescue.              |
+=====================================================================+

🏋️ Practice exercise

Your turn! Work through these tasks with paper first, then code.

  1. The vehicle and the driver. Model two dimensions: vehicles (Car, Bus) and driving styles (LearnerDriver drives slowly and honks before turns, ExpertDriver drives at normal speed). Make Driver the abstraction holding a Vehicle implementation with primitives like accelerate(kmph), steer(direction), and honk(). Show that both drivers can drive both vehicles — four pairings from four classes. Then add a Truck and count how many new classes you needed (the answer should be exactly one).

  2. The message and the channel. Extend the Python notification system from this post: add a WhatsAppChannel and a ReminderNotification that prefixes "Reminder:" to every message. Send a reminder by WhatsApp and an urgent alert by email. Then answer in one line: how many classes would you need for 4 notification types and 5 channels with Bridge, and how many without? (Check your answer against Figure 4.)

  3. Draw the state chart. For your vehicle exercise, draw a state diagram like Figure 8 for the vehicle (stopped, moving, honking). Then mark which side owns the states (implementation) and which side fires the events (abstraction). If your drawing matches that split, your bridge is healthy.

  4. Spot the explosion. Search your own code (or any project) for class names that contain two ideas joined together — names like PdfInvoiceExporter, CsvReportExporter, AdminWindowsMenu. For one such family, sketch on paper how you would split it into an abstraction and an implementation. You do not have to refactor — just drawing the two towers and the bridge reference is the real learning.

If the phrase "plus, not multiply" pops into your head the next time a class family starts doubling — the Bridge pattern is officially yours. Meera's drawer is empty, Aji's cupboard guards the old remotes, and one universal remote runs the whole house. Keep going, you are doing great!

Frequently asked questions

What is the Bridge pattern in simple words?
The Bridge pattern splits one big class family into two separate families that can grow on their own. One family is the high-level control side (the abstraction, like a remote), and the other is the low-level working side (the implementation, like a TV). They are connected by a reference, not by inheritance, so adding to one side never disturbs the other.
Why is it called a bridge?
The bridge is the reference (the has-a link) that connects the abstraction object to the implementation object. The remote holds a reference to a device and sends all real work across that link, just like traffic crossing a bridge between two banks of a river.
What problem does the Bridge pattern solve?
It stops the Cartesian explosion of subclasses. If you have 3 kinds of remotes and 4 kinds of devices, inheritance forces 12 combined classes. With Bridge you write only 3 + 4 = 7 classes, and any remote can drive any device.
How is Bridge different from Adapter?
Timing and intent. Bridge is planned in advance: you deliberately design two separate hierarchies so they can grow independently. Adapter is a rescue applied later, to make two already-finished, incompatible interfaces work together.
Can I change the implementation at runtime with Bridge?
Yes. Because the abstraction holds the implementation as a normal object reference, you can hand it a different implementation object while the program is running — like pointing the same remote at a different device.

Further reading

Related Lessons