Bridge Design Pattern

The Bridge design pattern is a structural pattern that helps separate an abstraction from its implementation, allowing both to evolve independently. It is useful when we have multiple dimensions of change in our system.
Key Concept
Abstraction: Defines the high-level control logic.
Implementation: Provides the concrete behaviour behind the abstraction.
The Bridge acts as an interface between these layers.
We can implement the Bridge pattern to decouple a high-level maps functionality from the underlying mapping provider (such as Google Maps). This setup allows us to easily swap out or extend providers (for example, to include ArcGIS Maps or OpenStreetMaps) without rewriting the application logic.
Step 1 : Define the Implementor Interface
//Implementor interface
export interface MapProvider {
initializeMap(elementId:string,center:{lat:number,lng:number},zoom:number):void;
addMarker(position: {lat:number,lng:number},title:string):void;
}
//This interface declares operations that concrete mapping providers must implement.
//The MapProvider interface defines a common structure for all map providers.
//This interface acts as the bridge that ensures all map providers implement the same methods.
MapProvider interface defines the implementation of the tasks needed from the map providers. Every map provider is supposed to initialise the map and add markers. The specific map provider that adheres to this required contract can do both the required jobs in a way that they like.
It’s the "engine" or "service" responsible for interfacing with specific map SDKs (Google Maps SDK, OpenStreetMap library, etc.).
Step 2: Create a Concrete Implementor for Google Maps
In this example, we will see a draft code of the Google Maps implementation for the sake of learning only. In an actual scenario, we require the Google Maps API Keys and other setup parameters that is vital for the implementation.
//Concrete Implementor
import { MapProvider } from "./MapProvider";
declare var google: any; // Assuming the Google Maps API is loaded
export class GoogleMapsProvider implements MapProvider {
private map:any;
initializeMap(elementId: string, center: { lat: number; lng: number; }, zoom: number): void {
this.map = new google.maps.Map(document.getElementById(elementId),{center,zoom});
console.log('google maps initialised');
}
addMarker(position: { lat: number; lng: number; }, title: string): void {
if(!this.map){
console.log('map not initialised');
return;
}
new google.maps.Marker({position,title});
console.log(`Marker added at position ${position.lat}, ${position.lng} : ${title}`);
}
}
The GoogleMapsProvider class adheres to the interface defined in step 1. The code snippet provided is a simplified version for the sake of learning purpose. The reader is encouraged to try out the code using Typescript API for google maps SDK and implement the same in a code base.
Step 3 : Create a concrete Implementor for OpenStreet Maps
// OpenStreetMapProvider.ts
import { MapProvider } from "./MapProvider";
import * as L from "leaflet";
export class OpenStreetMapProvider implements MapProvider {
private map: any;
initializeMap(elementId: string, center: { lat: number; lng: number; }, zoom: number): void {
// For example, using Leaflet to display OpenStreetMap
this.map = L.map(elementId).setView(center, zoom);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors"
}).addTo(this.map);
console.log("OpenStreetMap initialized.");
}
addMarker(position: { lat: number; lng: number }, title: string): void {
if (!this.map) {
console.error("Map is not initialized!");
return;
}
L.marker([position.lat, position.lng]).addTo(this.map).bindPopup(title);
console.log(`OpenStreetMap Marker added: ${title}`);
}
}
Just like Step 2, The OpenStreet Map also adheres to the interface in step 1 and has its own way of providing the two functionalities.
Step 4 : Create the Abstraction
This abstraction represents a higher-level map view that is independent of the specific mapping provider.
import { MapProvider } from "./MapProvider";
//the "bridge" class
export abstract class MapView{
protected provider : MapProvider | undefined;
constructor(provider : MapProvider){
this.provider = provider;
}
abstract displayMap(elementId:string):void;
abstract placeMarker(lat:number,lng:number,title:string):void;
}
Here we are trying to create a MapView class that serves as an abstraction layer over different map providers (Google Maps, OpenStreet Map, etc.). The idea is to provide a common interface for working with maps, no matter which specific map provider we are using. This makes the application more flexible and extensible.
Why an Abstract Class?
The MapView class is abstract, meaning it cannot be instantiated directly. It only provides a common blueprint for any subclass. This allows you to define certain methods that must be implemented by the subclasses (like displayMap and placeMarker), but leaves the implementation details up to them.
The MapView class is the “bridge” between the software application and the map providers. Any map provider must display the markers after the map initialisation.
The provider is a property that can hold a MapProvider object (e.g., Google Maps, OpenStreetMap, etc.).
MapProvider is an interface (or class) that defines the behaviour of different map providers. So, the provider will be any map provider that implements this interface.
undefined is assigned here initially, meaning when MapView is created, the provider may not be immediately available, but you’ll set it through the constructor.
MapView tells the app to show a map, add markers, and interact with the map in a way that doesn’t depend on whether you are using Google Maps or OpenStreetMap.
MapView could have different subclasses, like:
GoogleMapView: Implements how Google Maps will render and place markers.
OpenStreetMapView: Implements how OpenStreetMap will render and place markers.
Step 5 : Create a Refined Abstraction
This refined abstraction implements high-level behaviour using the underlying provider.
import { MapView } from "./MapView";
export class StandardMapView extends MapView {
private center = { lat: 18.779, lng: 72.4194 };
private zoom = 12;
displayMap(elementId: string): void {
this.provider?.initializeMap(elementId,this.center,this.zoom);
}
placeMarker(lat: number, lng: number, title: string): void {
this.provider?.addMarker({lat,lng},title);
}
}
//The StandardMapView extends MapView, providing higher-level control.
//It allows you to introduce new behaviors in StandardMapView without modifying the core providers.
Step 6 : Client Code
The client code can now work with the abstraction without caring about the underlying mapping implementation. To use Google Maps, simply inject the GoogleMapsProvider into your StandardMapView.
// Client code example
import { GoogleMapsProvider } from "./GoogleMapsProvider";
import { OpenStreetMapProvider } from "./OpenStreetMapProvider";
import { StandardMapView } from "./StandardMapView";
function main() {
// Assume there is a div with id "map" in your HTML
const googleProvider = new GoogleMapsProvider();
const openstreetmapProvider = new OpenStreetMapProvider();
const mapView = new StandardMapView(openstreetmapProvider);
mapView.displayMap("map"); // Initializes the Google Map
// Later in the application, add markers as needed
mapView.placeMarker(18.7749, 73.4194, "Mumbai");
mapView.placeMarker(33.89, 35.50, "Beirut");
}
main();
Benefits of Bridge Pattern
Decoupling: The high-level MapView logic is independent of the concrete provider (Google Maps or OpenStreet Maps in this case).
Flexibility: We can easily introduce another provider (e.g., ArcGIS Maps) by implementing the MapProvider interface and swapping the implementation without changes to the rest of the code.
Maintainability: This separation makes the system more maintainable and extensible as the mapping requirements evolve.
This example demonstrates a practical way to use the Bridge pattern for integrating Google Maps while keeping the application open to future changes in mapping services.
The GITHUB code link is here
NOTE : The code is a watered version and simplified just to highlight the significance of the Bridge pattern. The reader needs to use this as a “lighthouse” and prepare the actual code using Google Map API keys and follow the SDK rules. If the above code is simply compiled and run, it might not produce the desired result.
Subscribe to my newsletter
Read articles from Ganesh Rama Hegde directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ganesh Rama Hegde
Ganesh Rama Hegde
Passionate Developer | Code Whisperer | Innovator Hi there! I'm a senior software developer with a love for all things tech and a knack for turning complex problems into elegant, scalable solutions. Whether I'm diving deep into TypeScript, crafting seamless user experiences in React Native, or exploring the latest in cloud computing, I thrive on the thrill of bringing ideas to life through code. I’m all about creating clean, maintainable, and efficient code, with a strong focus on best practices like the SOLID principles. My work isn’t just about writing code; it’s about crafting digital experiences that resonate with users and drive impact. Beyond the code editor, I’m an advocate for continuous learning, always exploring new tools and technologies to stay ahead in this ever-evolving field. When I'm not coding, you'll find me blogging about my latest discoveries, experimenting with side projects, or contributing to open-source communities. Let's connect, share knowledge, and build something amazing together!