Loading content...
"Isn't Bridge just Adapter with extra steps?"
This question surfaces in nearly every design patterns discussion. Both patterns involve wrapping one interface to work with another. Both use composition. Both enable flexibility. The confusion is understandable—structurally, they look almost identical. A class holds a reference to another class through an interface.
But they serve fundamentally different purposes.
Adapter is about making incompatible things work together after the fact. Bridge is about designing flexibility in from the start. This distinction—reactive versus proactive—changes everything about when, why, and how you apply each pattern.
By the end of this page, you will clearly understand the differences between Bridge and Adapter patterns in terms of intent, timing, structure, and application. You'll be able to recognize when each pattern is appropriate, avoid common mistakes in pattern selection, and understand how both patterns can sometimes work together in the same system.
The most important difference between Bridge and Adapter is intent—the problem each pattern exists to solve.
The timing metaphor:
Adapter is a post-hoc solution. You have two existing systems that don't fit together. You build an adapter to bridge the gap. It's reactive—responding to an existing incompatibility.
Bridge is an a priori design. You anticipate two dimensions of variation before implementation. You structure the code from the beginning to accommodate this variation. It's proactive—preventing future problems.
Real-world analogy:
Adapter: You bought a European appliance but have American outlets. You buy a plug adapter. The appliance and outlet both already exist; the adapter makes them compatible.
Bridge: You're designing a smart home system. You create an abstract "Outlet" interface and abstract "Appliance" interface, knowing that outlets vary by country and appliances vary by function. From the start, any appliance can work with any outlet through the abstraction.
Am I dealing with existing incompatible interfaces that need to work together? → Adapter. Am I designing a system where both abstraction and implementation need to vary independently? → Bridge.
Despite different intents, Bridge and Adapter have similar structures. Let's examine them side-by-side to see both the similarities and the crucial differences.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
// ADAPTER PATTERN// Goal: Make LegacyPrinter work with the DocumentPrinter interface // The TARGET interface - what the client expectsinterface DocumentPrinter { printDocument(content: string, format: 'A4' | 'Letter'): void;} // The ADAPTEE - existing class with incompatible interface// This might be a third-party library or legacy code you can't modifyclass LegacyPrinter { // Different method names, different parameters oldStylePrint(text: string, width: number, height: number): void { console.log(`Legacy printing: "${text}" at ${width}x${height}mm`); }} // The ADAPTER - wraps Adaptee to provide Target interfaceclass LegacyPrinterAdapter implements DocumentPrinter { private legacyPrinter: LegacyPrinter; constructor(legacyPrinter: LegacyPrinter) { this.legacyPrinter = legacyPrinter; } printDocument(content: string, format: 'A4' | 'Letter'): void { // ADAPT: Convert Target interface to Adaptee interface const dimensions = format === 'A4' ? { width: 210, height: 297 } : { width: 216, height: 279 }; this.legacyPrinter.oldStylePrint(content, dimensions.width, dimensions.height); }} // Client code uses Target interface, unaware of Adapteefunction printReport(printer: DocumentPrinter, report: string): void { printer.printDocument(report, 'A4');} // Usage: Wrap legacy printer with adapterconst legacyPrinter = new LegacyPrinter();const adaptedPrinter = new LegacyPrinterAdapter(legacyPrinter);printReport(adaptedPrinter, "Quarterly Report"); // KEY CHARACTERISTICS:// 1. Single hierarchy on the Adapter side (no Refined Abstractions)// 2. Adaptee exists independently - we're wrapping, not designing// 3. One-to-one relationship: one Adaptee, one Adapter// 4. Focus: interface translation, not variation management123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
// BRIDGE PATTERN// Goal: Allow Documents and Printers to vary independently // The IMPLEMENTOR - interface for implementation hierarchyinterface PrinterImplementor { // Primitive printing operations print(content: string, widthMm: number, heightMm: number): void; getPrinterName(): string;} // CONCRETE IMPLEMENTORS - different printing implementationsclass LaserPrinterImpl implements PrinterImplementor { print(content: string, widthMm: number, heightMm: number): void { console.log(`[Laser] Printing "${content}" at ${widthMm}x${heightMm}mm`); } getPrinterName(): string { return "Laser Printer"; }} class InkjetPrinterImpl implements PrinterImplementor { print(content: string, widthMm: number, heightMm: number): void { console.log(`[Inkjet] Printing "${content}" at ${widthMm}x${heightMm}mm`); } getPrinterName(): string { return "Inkjet Printer"; }} class PdfPrinterImpl implements PrinterImplementor { print(content: string, widthMm: number, heightMm: number): void { console.log(`[PDF] Generating "${content}" as ${widthMm}x${heightMm}mm PDF`); } getPrinterName(): string { return "PDF Printer"; }} // The ABSTRACTION - document abstraction with reference to Implementorabstract class Document { protected printer: PrinterImplementor; // THE BRIDGE constructor(printer: PrinterImplementor) { this.printer = printer; } abstract print(): void;} // REFINED ABSTRACTIONS - different document typesclass Report extends Document { constructor( printer: PrinterImplementor, private title: string, private content: string ) { super(printer); } print(): void { const formatted = `=== ${this.title} ===${this.content}`; this.printer.print(formatted, 210, 297); // A4 }} class Invoice extends Document { constructor( printer: PrinterImplementor, private invoiceNumber: string, private amount: number ) { super(printer); } print(): void { const formatted = `Invoice #${this.invoiceNumber}Amount: $${this.amount}`; this.printer.print(formatted, 210, 297); }} class BusinessCard extends Document { constructor( printer: PrinterImplementor, private name: string, private title: string ) { super(printer); } print(): void { const formatted = `${this.name}${this.title}`; this.printer.print(formatted, 85, 55); // Business card size }} // Usage: Any document type × Any printer typeconst laser = new LaserPrinterImpl();const inkjet = new InkjetPrinterImpl();const pdf = new PdfPrinterImpl(); const reportOnLaser = new Report(laser, "Q4 Results", "Revenue increased...");const invoiceOnPdf = new Invoice(pdf, "INV-001", 1500);const cardOnInkjet = new BusinessCard(inkjet, "Jane Doe", "Senior Engineer"); reportOnLaser.print();invoiceOnPdf.print();cardOnInkjet.print(); // KEY CHARACTERISTICS:// 1. TWO hierarchies: Document (Abstraction) and Printer (Implementor)// 2. Both hierarchies designed together from the start// 3. Many-to-many: Any abstraction can use any implementation// 4. Focus: independent variation of two dimensions| Aspect | Adapter Pattern | Bridge Pattern |
|---|---|---|
| Number of Hierarchies | One (Adapter) | Two (Abstraction + Implementor) |
| Adaptee/Implementor | Pre-existing, can't modify | Designed as part of pattern |
| Wrapper Relationship | One-to-one (specific adaptation) | Many-to-many (any combination) |
| Interface Design | Matches existing Target | Primitives designed for reuse |
| Evolution | Adaptee evolves independently | Both hierarchies evolve together |
The timing of when you apply each pattern is perhaps the clearest differentiator.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Scenario: You're building a payment system// Your code expects this interface:interface PaymentProcessor { processPayment(amount: number, currency: string): Promise<PaymentResult>; refund(transactionId: string): Promise<RefundResult>;} // But you need to integrate Stripe, which has its own SDK:// (You can't modify Stripe's code - it's a third-party library)class Stripe { charges = { create: async (params: { amount: number; currency: string }) => { // Stripe's implementation return { id: 'ch_123', status: 'succeeded' }; } }; refunds = { create: async (params: { charge: string }) => { return { id: 're_123', status: 'succeeded' }; } };} // ADAPTER: Wrap Stripe to match your interfaceclass StripePaymentAdapter implements PaymentProcessor { private stripe: Stripe; constructor(stripe: Stripe) { this.stripe = stripe; } async processPayment(amount: number, currency: string): Promise<PaymentResult> { // Adapt: Convert your interface to Stripe's interface const result = await this.stripe.charges.create({ amount: amount * 100, // Stripe uses cents currency: currency.toLowerCase() }); return { success: result.status === 'succeeded', transactionId: result.id }; } async refund(transactionId: string): Promise<RefundResult> { const result = await this.stripe.refunds.create({ charge: transactionId }); return { success: true, refundId: result.id }; }} // Why Adapter and not Bridge?// - Stripe already exists with a fixed interface// - You're solving ONE integration problem// - You're not designing for "payment processors × payment types" variation123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
// Scenario: You're BUILDING a payment system from scratch// You anticipate supporting multiple payment TYPES and multiple PROVIDERS // IMPLEMENTOR: Provider implementation interfaceinterface PaymentProviderImpl { // Primitive operations that all providers must support charge(amountCents: number, currency: string, token: string): Promise<ProviderResult>; refundCharge(chargeId: string, amountCents: number): Promise<ProviderResult>; getProviderName(): string;} // CONCRETE IMPLEMENTORS: Different providersclass StripeProviderImpl implements PaymentProviderImpl { async charge(amountCents: number, currency: string, token: string): Promise<ProviderResult> { console.log(`Stripe: Charging ${amountCents} ${currency}`); return { success: true, externalId: 'ch_stripe_123' }; } async refundCharge(chargeId: string, amountCents: number): Promise<ProviderResult> { return { success: true, externalId: 're_stripe_123' }; } getProviderName(): string { return 'Stripe'; }} class PayPalProviderImpl implements PaymentProviderImpl { async charge(amountCents: number, currency: string, token: string): Promise<ProviderResult> { console.log(`PayPal: Charging ${amountCents} ${currency}`); return { success: true, externalId: 'pp_123' }; } async refundCharge(chargeId: string, amountCents: number): Promise<ProviderResult> { return { success: true, externalId: 'pp_ref_123' }; } getProviderName(): string { return 'PayPal'; }} // ABSTRACTION: Payment type abstractionabstract class Payment { protected provider: PaymentProviderImpl; // THE BRIDGE constructor(provider: PaymentProviderImpl) { this.provider = provider; } abstract process(): Promise<PaymentResult>; abstract refund(): Promise<RefundResult>;} // REFINED ABSTRACTIONS: Different payment typesclass OneTimePayment extends Payment { constructor( provider: PaymentProviderImpl, private amount: number, private currency: string, private paymentToken: string ) { super(provider); } private chargeId?: string; async process(): Promise<PaymentResult> { const result = await this.provider.charge( Math.round(this.amount * 100), this.currency, this.paymentToken ); this.chargeId = result.externalId; return { success: result.success, transactionId: result.externalId }; } async refund(): Promise<RefundResult> { if (!this.chargeId) throw new Error('No charge to refund'); const result = await this.provider.refundCharge( this.chargeId, Math.round(this.amount * 100) ); return { success: result.success, refundId: result.externalId }; }} class SubscriptionPayment extends Payment { constructor( provider: PaymentProviderImpl, private monthlyAmount: number, private currency: string, private paymentToken: string ) { super(provider); } async process(): Promise<PaymentResult> { // Subscription-specific logic console.log(`Setting up monthly charge of ${this.monthlyAmount}`); const result = await this.provider.charge( Math.round(this.monthlyAmount * 100), this.currency, this.paymentToken ); return { success: result.success, transactionId: result.externalId }; } async refund(): Promise<RefundResult> { // Subscriptions might prorate refunds throw new Error('Use cancelSubscription() instead'); }} // ANY payment type × ANY providerconst stripeOneTime = new OneTimePayment(new StripeProviderImpl(), 99.99, 'USD', 'tok_1');const paypalSubscription = new SubscriptionPayment(new PayPalProviderImpl(), 9.99, 'USD', 'tok_2'); // Why Bridge and not Adapter?// - You're DESIGNING both hierarchies from scratch// - You anticipate MULTIPLE payment types AND MULTIPLE providers// - Both sides will grow independently over timeAnother key difference lies in what each side of the pattern knows about the other.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
// ADAPTER: The Adapter knows EVERYTHING about the Adaptee class XmlDataAdapter implements JsonDataSource { private xmlParser: LegacyXmlParser; // Adapter knows Adaptee intimately getData(): JsonObject { // Adapter knows exactly how the Adaptee works const xmlString = this.xmlParser.loadData(); const xmlDoc = this.xmlParser.parseXml(xmlString); const elements = this.xmlParser.getElements(xmlDoc); // Complex conversion logic specific to this Adaptee return this.convertXmlElementsToJson(elements); } private convertXmlElementsToJson(elements: any[]): JsonObject { // Detailed knowledge of XML structure and quirks }} // The Adaptee knows NOTHING about the Adapter - it predates it // ═══════════════════════════════════════════════════════════════════════════ // BRIDGE: The Abstraction knows only the Implementor INTERFACE abstract class RemoteControl { protected device: Device; // Only knows interface, not concrete type constructor(device: Device) { this.device = device; } togglePower(): void { if (this.device.isEnabled()) { this.device.disable(); } else { this.device.enable(); } } // RemoteControl doesn't know if it's a TV, Radio, or SmartLight // It only knows the Device interface operations} // The Implementor knows NOTHING about the Abstraction interface Device { isEnabled(): boolean; enable(): void; disable(): void; getVolume(): number; setVolume(percent: number): void;} class TV implements Device { // TV doesn't know it's being controlled by a RemoteControl // It just implements primitive operations private on: boolean = false; private volume: number = 50; isEnabled(): boolean { return this.on; } enable(): void { this.on = true; } disable(): void { this.on = false; } getVolume(): number { return this.volume; } setVolume(percent: number): void { this.volume = percent; }} // This bidirectional ignorance is intentional in Bridge:// - Abstraction doesn't care which concrete Implementor it has// - Implementor doesn't care which Abstraction is using it// → Maximum decoupling and flexibility| Pattern | Wrapper Knows About Wrapped | Wrapped Knows About Wrapper |
|---|---|---|
| Adapter | Yes — intimate knowledge of Adaptee internals | No — Adaptee predates Adapter |
| Bridge | No — only knows Implementor interface | No — Implementor is interface-based |
In Adapter, changing the Adaptee often requires changing the Adapter—they're tightly coupled by necessity. In Bridge, changing a Concrete Implementor doesn't affect the Abstraction at all, as long as the Implementor interface is stable. This is why Bridge scales better for ongoing evolution.
Understanding the differences helps avoid common mistakes in pattern selection.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// WRONG: Creating adapters for every combination// This is Adapter abuse - you're not adapting, you're designing class MySqlUserRepositoryAdapter implements UserRepository { private mysql: MySqlConnection; async findUser(id: string): Promise<User> { /* ... */ }} class PostgresUserRepositoryAdapter implements UserRepository { private postgres: PostgresConnection; async findUser(id: string): Promise<User> { /* ... */ }} class MySqlOrderRepositoryAdapter implements OrderRepository { private mysql: MySqlConnection; async findOrder(id: string): Promise<Order> { /* ... */ }} class PostgresOrderRepositoryAdapter implements OrderRepository { private postgres: PostgresConnection; async findOrder(id: string): Promise<Order> { /* ... */ }} // 4 adapters for 2 × 2... this will explode // CORRECT: Use Bridge patterninterface DatabaseConnection { // Implementor query(sql: string, params: any[]): Promise<any[]>; execute(sql: string, params: any[]): Promise<void>;} class MySqlConnection implements DatabaseConnection { /* ... */ }class PostgresConnection implements DatabaseConnection { /* ... */ } abstract class Repository { // Abstraction protected db: DatabaseConnection; constructor(db: DatabaseConnection) { this.db = db; }} class UserRepository extends Repository { async findUser(id: string): Promise<User> { const rows = await this.db.query('SELECT * FROM users WHERE id = ?', [id]); return this.mapToUser(rows[0]); }} class OrderRepository extends Repository { async findOrder(id: string): Promise<Order> { const rows = await this.db.query('SELECT * FROM orders WHERE id = ?', [id]); return this.mapToOrder(rows[0]); }} // 2 + 2 = 4 classes instead of 2 × 2 = 4... but with 5 repos and 3 DBs:// Bridge: 5 + 3 = 8 vs Adapter abuse: 5 × 3 = 15123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// WRONG: Overengineering for a simple integration// You just need to wrap one analytics library, that's it // Unnecessary abstraction hierarchyabstract class AnalyticsEvent { protected impl: AnalyticsImplementor; constructor(impl: AnalyticsImplementor) { this.impl = impl; } abstract track(): void;} class PageViewEvent extends AnalyticsEvent { track(): void { /* ... */ }} class ClickEvent extends AnalyticsEvent { track(): void { /* ... */ }} interface AnalyticsImplementor { sendEvent(name: string, properties: object): void;} class GoogleAnalyticsImpl implements AnalyticsImplementor { sendEvent(name: string, properties: object): void { /* ... */ }} // This is Bridge overkill for: "I need to use Google Analytics in my app" // CORRECT: Simple Adapter is enoughinterface Analytics { trackPageView(page: string): void; trackClick(element: string): void;} class GoogleAnalyticsAdapter implements Analytics { private ga: GoogleAnalyticsSDK; trackPageView(page: string): void { this.ga.send('pageview', { page }); } trackClick(element: string): void { this.ga.send('event', { action: 'click', label: element }); }} // Clean and simple. If you later need Mixpanel, add MixpanelAdapter.// Don't over-architect until you actually have multiple dimensions of variation.Don't use Bridge "just in case" you might need multiple implementations. Start with a simple interface and Adapter for integration. Refactor to Bridge if and when you actually encounter multiple dimensions of variation that are causing class explosion.
Use this decision framework to choose between Bridge and Adapter:
┌──────────────────────────────────┐ │ Do you have an EXISTING interface│ │ that needs to work with your code│ │ (third-party, legacy, external)? │ └───────────────┬──────────────────┘ │ ┌───────────────┴───────────────┐ │ │ ▼ ▼ YES NO │ │ ┌───────────┴───────────┐ ┌──────────┴──────────┐ │ Can you modify the │ │ Are you designing a │ │ existing code? │ │ new system with TWO+ │ │ │ │ dimensions of │ │ │ │ variation? │ └───────────┬───────────┘ └──────────┬──────────┘ │ │ ┌───────────┴───────────┐ ┌──────────┴──────────┐ │ │ │ │ ▼ ▼ ▼ ▼ YES NO YES NO │ │ │ │ ▼ ▼ ▼ ▼┌───────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────────┐│ Consider │ │ │ │ │ │ Start simple ││ refactoring │ │ ADAPTER │ │ BRIDGE │ │ Single class ││ to common │ │ │ │ │ │ or interface ││ interface │ │ │ │ │ │ │└───────────────┘ └───────────┘ └───────────┘ └───────────────┘ ADDITIONAL QUESTIONS: ┌─────────────────────────────────────────────────────────────────────────────┐│ QUESTION │ YES → BRIDGE │ NO → ? │├────────────────────────────────────────────────────┼──────────────┼─────────┤│ Will abstraction variants grow over time? │ ✓ │ Adapter ││ Will implementation variants grow over time? │ ✓ │ Adapter ││ Do you need runtime switching of implementation? │ ✓ │ Adapter ││ Are you integrating ONE third-party library? │ Adapter │ ✓ ││ Is the interface mismatch your only problem? │ Adapter │ ✓ │└─────────────────────────────────────────────────────────────────────────────┘Bridge and Adapter aren't mutually exclusive. In fact, they often work together elegantly: Bridge for overall structure, Adapter for integrating external systems into the Bridge's Implementor hierarchy.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
// SCENARIO: Building a multi-cloud storage system// - Multiple storage ABSTRACTIONS (File, Block, Object)// - Multiple IMPLEMENTATIONS (AWS, Azure, GCP)// - Some implementations are third-party SDKs that need adapting // ═══════════════════════════════════════════════════════════════════════════// BRIDGE PATTERN: Overall architecture// ═══════════════════════════════════════════════════════════════════════════ // IMPLEMENTOR: Our storage operations interfaceinterface StorageImplementor { upload(key: string, data: Buffer): Promise<string>; download(key: string): Promise<Buffer>; delete(key: string): Promise<void>; list(prefix: string): Promise<string[]>;} // ABSTRACTION: Storage abstraction hierarchyabstract class Storage { protected impl: StorageImplementor; // BRIDGE constructor(impl: StorageImplementor) { this.impl = impl; } abstract store(item: any): Promise<string>; abstract retrieve(id: string): Promise<any>;} // REFINED ABSTRACTIONSclass FileStorage extends Storage { async store(file: { name: string; content: Buffer }): Promise<string> { return this.impl.upload(`files/${file.name}`, file.content); } async retrieve(fileName: string): Promise<{ name: string; content: Buffer }> { const content = await this.impl.download(`files/${fileName}`); return { name: fileName, content }; }} class DocumentStorage extends Storage { async store(doc: { id: string; data: object }): Promise<string> { const json = JSON.stringify(doc.data); return this.impl.upload(`docs/${doc.id}.json`, Buffer.from(json)); } async retrieve(docId: string): Promise<{ id: string; data: object }> { const content = await this.impl.download(`docs/${docId}.json`); return { id: docId, data: JSON.parse(content.toString()) }; }} // ═══════════════════════════════════════════════════════════════════════════// ADAPTER PATTERN: Integrating third-party SDKs into our Implementor hierarchy// ═══════════════════════════════════════════════════════════════════════════ // Third-party AWS SDK (can't modify)class AWSS3Client { async putObject(bucket: string, key: string, body: Buffer): Promise<{ ETag: string }> { console.log(`[AWS S3] Uploading to ${bucket}/${key}`); return { ETag: 'etag123' }; } async getObject(bucket: string, key: string): Promise<{ Body: Buffer }> { console.log(`[AWS S3] Downloading ${bucket}/${key}`); return { Body: Buffer.from('content') }; } async deleteObject(bucket: string, key: string): Promise<void> { console.log(`[AWS S3] Deleting ${bucket}/${key}`); } async listObjectsV2(bucket: string, prefix: string): Promise<{ Contents: { Key: string }[] }> { return { Contents: [{ Key: 'file1' }, { Key: 'file2' }] }; }} // ADAPTER: Adapts AWS SDK to our StorageImplementor interfaceclass AWSS3Adapter implements StorageImplementor { private s3: AWSS3Client; private bucket: string; constructor(s3: AWSS3Client, bucket: string) { this.s3 = s3; this.bucket = bucket; } async upload(key: string, data: Buffer): Promise<string> { const result = await this.s3.putObject(this.bucket, key, data); return result.ETag; } async download(key: string): Promise<Buffer> { const result = await this.s3.getObject(this.bucket, key); return result.Body; } async delete(key: string): Promise<void> { await this.s3.deleteObject(this.bucket, key); } async list(prefix: string): Promise<string[]> { const result = await this.s3.listObjectsV2(this.bucket, prefix); return result.Contents.map(c => c.Key); }} // Third-party Azure SDK (can't modify)class AzureBlobClient { async uploadBlob(container: string, blobName: string, content: Buffer): Promise<string> { console.log(`[Azure Blob] Uploading to ${container}/${blobName}`); return 'azure-url'; } async downloadBlob(container: string, blobName: string): Promise<Buffer> { return Buffer.from('azure content'); } async deleteBlob(container: string, blobName: string): Promise<void> { } async listBlobs(container: string, prefix: string): Promise<string[]> { return ['blob1', 'blob2']; }} // ADAPTER: Adapts Azure SDK to our StorageImplementor interfaceclass AzureBlobAdapter implements StorageImplementor { private azure: AzureBlobClient; private container: string; constructor(azure: AzureBlobClient, container: string) { this.azure = azure; this.container = container; } async upload(key: string, data: Buffer): Promise<string> { return this.azure.uploadBlob(this.container, key, data); } async download(key: string): Promise<Buffer> { return this.azure.downloadBlob(this.container, key); } async delete(key: string): Promise<void> { await this.azure.deleteBlob(this.container, key); } async list(prefix: string): Promise<string[]> { return this.azure.listBlobs(this.container, prefix); }} // ═══════════════════════════════════════════════════════════════════════════// USAGE: Bridge structure with Adapted implementations// ═══════════════════════════════════════════════════════════════════════════ // Create adapters for each cloud providerconst awsAdapter = new AWSS3Adapter(new AWSS3Client(), 'my-bucket');const azureAdapter = new AzureBlobAdapter(new AzureBlobClient(), 'my-container'); // Bridge: Any abstraction × Any adapted implementationconst awsFileStorage = new FileStorage(awsAdapter);const azureDocStorage = new DocumentStorage(azureAdapter);const awsDocStorage = new DocumentStorage(awsAdapter); // All combinations work!await awsFileStorage.store({ name: 'report.pdf', content: Buffer.from('...') });await azureDocStorage.store({ id: 'doc1', data: { title: 'Hello' } });await awsDocStorage.retrieve('doc2'); // STRUCTURE SUMMARY:// └── Bridge Pattern (overall architecture)// ├── Abstraction: Storage// │ ├── Refined: FileStorage// │ └── Refined: DocumentStorage// └── Implementor: StorageImplementor// ├── Adapter: AWSS3Adapter (wraps AWSS3Client)// └── Adapter: AzureBlobAdapter (wraps AzureBlobClient)In this design, Bridge provides the extensible architecture for storage types × storage backends, while Adapters handle the messy work of integrating third-party SDKs into our clean Implementor interface. Adding a new storage type (e.g., ImageStorage) requires no adapter changes. Adding a new cloud (e.g., GCP) requires only one new adapter.
Let's crystallize the key differences between Bridge and Adapter:
| Dimension | Adapter Pattern | Bridge Pattern |
|---|---|---|
| Intent | Make incompatible interfaces work together | Separate abstraction from implementation |
| Timing | Reactive (after interfaces exist) | Proactive (during design) |
| Hierarchies | One (the adapter) | Two (abstraction + implementor) |
| Wrapped Object | Pre-existing, can't modify | Designed as part of pattern |
| Relationship | One-to-one adaptation | Many-to-many composition |
| Knowledge | Adapter intimately knows Adaptee | Abstraction only knows interface |
| Evolution | Adaptee evolves independently | Both sides designed to evolve together |
| Class Count | One adapter per adaptation | M + N instead of M × N |
| Primary Use | Integration with external systems | Internal architecture design |
What's next:
With a solid understanding of Bridge's structure and how it differs from Adapter, we're ready to explore real-world use cases and examples. In the next page, we'll see Bridge applied to practical scenarios including UI frameworks, device control systems, and more.
You now clearly understand the differences between Bridge and Adapter patterns. Bridge is a proactive design pattern for separating two dimensions of variation; Adapter is a reactive integration pattern for making incompatible interfaces work together. This clarity will help you choose the right pattern for each situation.