The Elephant In The Room

You’re going to read this and think, wow this is super similar to the Strategy pattern — and you’re right, it is. It’s almost identical. See my write-up on Strategy here.

But here’s the thing:

It depends on context.

The core mechanics — swapping implementations at runtime, using an interface, injecting dependencies — yeah, those look like a twin set of design patterns wearing different hats. But the intent behind each pattern is what separates them.

  • Strategy is about choosing an algorithm or behaviour dynamically.
    “How should I do this one thing?”

  • Bridge is about decoupling abstraction from implementation.
    “I want the freedom to evolve both sides without locking them together.”

Same tools. Different goals.

Why Does That Matter?

Because Strategy is meant to be swapped often, even mid-flight. It's about flexibility of behaviour.

Bridge, on the other hand, is about long-term architectural separation — keeping the abstraction (your high-level logic) free from the implementation (your low-level code) so they can evolve separately.

  • Strategy: Focuses on changing how something works.

  • Bridge: Focuses on keeping parts of your code separate so they don’t rely on each other too much.

Both are powerful. Both are useful. And yes, they look wildly similar. But next time you reach for one, ask yourself:

Am I trying to swap out a behaviour, or am I separating layers of responsibility?

That's the difference.

With all that in mind, let’s look at a real-world problem it solves.

A Real-World Problem

Let’s say you’re designing a universal remote control system.

You’ve got different remotes:

  • BasicRemote

  • AdvancedRemote (can mute, change input, etc.)

And different devices:

  • TV

  • Radio

You might end up with something like this:

class TVRemote {
    public function turnOn() { /* TV logic */ }
}

class RadioRemote {
    public function turnOn() { /* Radio logic */ }
}

That’s manageable. But now your product owner walks in and says:

"Hey, we want an Advanced Remote that can also mute devices."

So you go:

class AdvancedTVRemote extends TVRemote {
    public function mute() { return "Muting TV"; }
}

class AdvancedRadioRemote extends RadioRemote {
    public function mute() { return "Muting radio"; }
}

Suddenly, you’ve got duplicated logic and almost identical methods being re-implemented across very specific subclasses.

Now imagine you support:

  • TV

  • Radio

  • Projector

  • Soundbar

And each of those needs:

  • A Basic Remote

  • An Advanced Remote

  • A Gaming Remote

  • A Voice-Controlled Remote

You’re now staring down the barrel of 4 devices × 4 remotes = 16 classes. But why is this bad?

  • High maintenance: Add a new feature to all remotes? Update 4+ classes.

  • Duplication: mute() is identical in AdvancedTVRemote, AdvancedRadioRemote, etc.

  • Tightly coupled: Your "remote" logic is hard-wired to a specific device.

  • Scalability nightmare: Every new device or remote feature multiplies the number of classes needed.

You’re no longer designing a system — you’re managing a zoo of permutations.

The Bridge Pattern

In this example, the Bridge pattern is saying:

“Lets separate your Remote hierarchy from your Device hierarchy. Let them vary independently.”

Instead of 16 classes, you have:

  • 4 Device classes (TV, Radio, etc.)

  • 4 Remote classes (Basic, Advanced, etc.)

  • And you plug them together at runtime like LEGO

It’s cleaner, leaner, and lets you expand in both directions without creating a mess. So how do we do this?

Step 1: Define the Device Interface

We want to treat all devices the same (TV, Radio, etc.)

interface Device
{
    public function turnOn(): string;
    public function mute(): string;
}

Step 2: Implement Concrete Devices

class Television implements Device
{
    public function turnOn(): string
    {
        return "Turning on the TV";
    }

    public function mute(): string
    {
        return "Muting the TV";
    }
}

class Radio implements Device
{
    public function turnOn(): string
    {
        return "Turning on the radio";
    }

    public function mute(): string
    {
        return "Muting the radio";
    }
}

Step 3: Create the Remote Abstraction (Bridge Part)

We create a RemoteControl that delegates to the Device.

class RemoteControl
{
    public function __construct(protected Device $device)
    {
        //
    }

    public function turnOn(): string
    {
        return $this->device->turnOn();
    }
}

Now, the remote doesn’t care what kind of device it’s controlling. Plug in a TV, Radio, whatever — the API stays the same.

Step 4: Create an Advanced Remote

class AdvancedRemoteControl extends RemoteControl
{
    public function mute(): string
    {
        return $this->device->mute();
    }
}

This advanced remote adds new features without needing to know the device implementation details.

Step 5: Using It All

$tv = new Television();
$radio = new Radio();

$basicRemote = new RemoteControl($tv);
echo $basicRemote->turnOn(); // "Turning on the television"

$advancedRemote = new AdvancedRemoteControl($radio);
echo $advancedRemote->turnOn(); // "Turning on the radio"
echo $advancedRemote->mute();   // "Muting the radio"

Instead of creating 16 specific remote subclasses (TVRemote, AdvancedTVRemote, GamingRadioRemote, etc.), you’ve:

  • Separated responsibilities: Remote controls and Devices.

  • Enabled flexibility: swap devices in and out

  • Reduced class count: no more exponential growth

Want to add voice control? Just extend RemoteControl.

Want to add a new device? Just implement Device.

You grow your app horizontally, not diagonally and exponentially.

Want to see the Bridge Pattern in action? Check out this simple PHP example: Github