Bridge vs Adapter Pattern
Bridge and Adapter look similar but solve different problems. Understanding when to use each is crucial.
Key Differences
| Aspect | Adapter | Bridge |
|---|---|---|
| Purpose | Make incompatible interfaces work together | Separate abstraction from implementation |
| When designed | After classes exist | Before classes exist |
| Focus | Interface compatibility | Extensibility |
| Relationship | Wraps existing code | Designed from scratch |
Adapter: Retrofitting
Adapter is reactive - you have existing code that doesn't fit:
typescript1// Existing third-party API (can't change)2class FedExTracker {3 getStatus(trackingId: string): FedExStatus {4 return { code: 'IN_TRANSIT', location: 'Memphis, TN' };5 }6}78// Your interface (established in your codebase)9interface TrackingService {10 track(id: string): TrackingInfo;11}1213// Adapter bridges the gap14class FedExTrackingAdapter implements TrackingService {15 constructor(private fedex: FedExTracker) {}1617 track(id: string): TrackingInfo {18 const status = this.fedex.getStatus(id);19 return {20 status: this.mapStatus(status.code),21 location: status.location22 };23 }2425 private mapStatus(code: string): string {26 const map: Record<string, string> = {27 'IN_TRANSIT': 'In Transit',28 'DELIVERED': 'Delivered'29 };30 return map[code] ?? 'Unknown';31 }32}
Bridge: Planning Ahead
Bridge is proactive - you design for variation upfront:
typescript1// You know you'll have multiple notification types AND channels2// Design the bridge from the start34interface NotificationChannel {5 send(to: string, message: Message): Promise<void>;6}78abstract class Notification {9 constructor(protected channel: NotificationChannel) {}10 abstract prepare(data: EventData): Message;1112 async send(recipient: string, data: EventData): Promise<void> {13 const message = this.prepare(data);14 await this.channel.send(recipient, message);15 }16}1718// Now you can freely add types and channels19class ShipmentNotification extends Notification { }20class DelayNotification extends Notification { }2122class EmailChannel implements NotificationChannel { }23class SMSChannel implements NotificationChannel { }24class PushChannel implements NotificationChannel { }
Visual Comparison
Adapter (wrapping):
1┌──────────┐ ┌───────────┐ ┌──────────┐2│ Client │─────►│ Adapter │─────►│ Adaptee │3└──────────┘ └───────────┘ └──────────┘4 (converts) (existing)
Bridge (separating):
1┌─────────────────┐ ┌─────────────────┐2│ Abstraction │────────►│ Implementor │3└────────┬────────┘ └────────┬────────┘4 │ │5 ┌────┴────┐ ┌────┴────┐6 │ │ │ │7┌───▼───┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───┐8│ Ref A │ │ Ref B │ │Impl 1 │ │Impl 2 │9└───────┘ └───────┘ └───────┘ └───────┘
Decision Guide
Use Adapter when:
- Integrating with existing, unchangeable code
- Working with third-party libraries
- Making legacy code fit new interfaces
- You need a one-to-one interface mapping
Use Bridge when:
- Designing a new system with multiple dimensions
- Both abstraction and implementation will vary
- You want to avoid class explosion from inheritance
- Runtime binding between abstraction and implementation
Combined Example
Sometimes you need both patterns:
typescript1// Third-party SMS provider (adapt it)2class TwilioSDK {3 sendMessage(to: string, from: string, text: string): void { }4}56// Adapter for Twilio7class TwilioAdapter implements NotificationChannel {8 constructor(private twilio: TwilioSDK, private fromNumber: string) {}910 async send(to: string, message: Message): Promise<void> {11 this.twilio.sendMessage(to, this.fromNumber, message.body);12 }13}1415// Use the adapted channel in the Bridge16const smsChannel = new TwilioAdapter(new TwilioSDK(), '+1234567890');17const notification = new ShipmentNotification(smsChannel);1819// Bridge lets you swap channels20// Adapter lets you use Twilio with your channel interface
Summary
- Adapter: "Make this existing thing fit my interface"
- Bridge: "Design so I can mix and match dimensions"
Both promote loose coupling, but Adapter fixes compatibility issues while Bridge prevents them by design.