Bridge - Design Pattern

Bridge - Design Pattern

Design Patterns: Elements of Reusable Object-Oriented Software


3 min read

If get to know something new by reading my articles, don't forget to endorse me on LinkedIn

What is Bridge Design Pattern?

"Decouple an abstraction from its implementation so that the two can vary independently."

When we use inheritance, we are permanently binding the implementation to the abstraction. As a result, any change made to one affects the other. A more flexible way is to separate the abstraction and the implementation, and this is where the bridge pattern comes in.

The objective of the bridge pattern is to identify how to realize relationships between classes and objects in a simple way. The bridge pattern does it by separating the abstraction and the implementation in separate class hierarchies. The bridge between the class hierarchies is achieved through composition.

Let's deep dive into Bridge Design Pattern.


Take a look at the above diagram, We see an abstract class Message which is extended by TextMessage and EmailMessage. Now, both TextMessage and EmailMessage is extended by TextMessageSender and EmailMessageSender respectively.

Everything is okay, right? Is there anything wrong with the design?

Notice that the abstraction part and the implementation part are tightly coupled. Our design relies on inheritance and one inherent disadvantages is that it breaks encapsulation.

What if client want's to add new layer of Encryption before sending the Message?. We need to update the abstraction part to make encryption functionality available to clients.

Another issue is reusability. If we want to reuse only the implementation (message sending) part in some other application, we will have to take along the abstraction part as extra baggage.

The bridge pattern addresses all such issues by separating the abstraction and implementation into two class hierarchies.


Now take look at the above diagram.

With the bridge pattern, the abstraction maintains a Has-A relationship with the implementation instead of a IS-A relationship. The Has-A relationship is achieved through composition where the abstraction maintains a reference of the implementation and forwards client requests to it.

Okay, lets get into coding part.

interface MessageSender {
    fun sendMessage()
abstract class Message(internal val sender: MessageSender) {
    abstract fun send()
class TextMessage(messageSender: MessageSender) : Message(messageSender) {
    override fun send() { sender.sendMessage() }
class EmailMessage(messageSender: MessageSender) : Message(messageSender) {
    override fun send() { sender.sendMessage() }

We have an abstract class Message which takes MessageSender as a parameter (Composition) and one abstract method send().

TextMessage, EmailMessage both extend Message and override the send() method and we are using the MessageSender object to send message (Text/Email).

Here, we are not tightly coupled to another subsystem, instead we followed Compositon.

class TextMessageSender : MessageSender {
    override fun sendMessage() {
        println("TextMessageSender: Sending text message...")
class EmailMessageSender : MessageSender {
    override fun sendMessage() {
        println("EmailMessageSender: Sending email message...")

Now, we have implementation of MessageSender for Text and Email Message. We, decoupled the nested logic which has been shown in the 1st diagram.

TextMessageSender and EmailMessageSender are the concrete classes of MessageSender. Now we can add Encryption before sending Message without any changes in Base.

Let's test it.

val textMessageSender: MessageSender = TextMessageSender()
val textMessage: Message = TextMessage(textMessageSender)
//Result - TextMessageSender: Sending text message...

val emailMessageSender: MessageSender = EmailMessageSender()
val emailMessage: Message = EmailMessage(emailMessageSender)
//Result - EmailMessageSender: Sending email message...

Finally, we did it. Now Message Has-A Relationship with MessageSender instead of Is-A Relationship and separated the abstraction and the implementation in separate class hierarchies.

Now, let's take a look at the full implementation in Kotlin.

Follow me on LinkedIn