Back to blog

Adapter and Facade Patterns Explained

oopstructural-patternsdesign-patternstypescriptpythonjava
Adapter and Facade Patterns Explained

Introduction

You've mastered creational patterns — Singleton, Factory, Builder, Prototype. Now it's time for structural patterns: patterns about how objects and classes are composed into larger structures.

Two structural patterns show up everywhere in real codebases:

PatternOne-Line Summary
AdapterMakes an incompatible interface work where a different interface is expected
FacadeProvides a simple interface to a complex subsystem

Both are about interfaces — but they solve opposite problems. Adapter converts an interface. Facade simplifies one.

What You'll Learn

✅ Understand the problem each pattern solves
✅ Implement Object Adapter and Class Adapter variants
✅ Build a Facade that hides subsystem complexity
✅ Apply both patterns in TypeScript, Python, and Java
✅ Choose between Adapter, Facade, and other structural patterns
✅ Avoid common mistakes when wrapping interfaces

Prerequisites


Part 1: The Adapter Pattern

The Problem: Incompatible Interfaces

You're building a payment system. Your entire codebase expects a PaymentProcessor interface:

interface PaymentProcessor {
  charge(amount: number, currency: string): PaymentResult;
}

Everything works — checkout, subscriptions, refunds — all coded against PaymentProcessor.

Now your boss says: "We're adding Stripe." But Stripe's SDK doesn't implement your PaymentProcessor interface. It has its own API:

// Stripe's SDK — you can't modify this
class StripeAPI {
  createCharge(amountInCents: number, cur: string, idempotencyKey: string): StripeResponse {
    // Stripe-specific logic
  }
}

Without the Adapter pattern, you'd litter your codebase with conditionals:

// ❌ Every checkout function becomes coupled to every payment provider
function checkout(cart: Cart, provider: string) {
  if (provider === "internal") {
    internalProcessor.charge(cart.total, "USD");
  } else if (provider === "stripe") {
    stripeAPI.createCharge(cart.total * 100, "USD", generateKey());
  } else if (provider === "paypal") {
    paypalSDK.makePayment({ value: cart.total, currency_code: "USD" });
  }
  // Every new provider = every checkout function modified ❌
}

The Adapter pattern wraps the incompatible class to make it fit the expected interface — without modifying either side.


Pattern Structure

Participants:

  • Target — the interface your code expects (PaymentProcessor)
  • Adaptee — the class with the incompatible interface (StripeAPI)
  • Adapter — wraps the adaptee and implements the target interface (StripeAdapter)
  • Client — works with the target interface, unaware of the adapter

Object Adapter vs Class Adapter

There are two ways to implement an adapter:

VariantMechanismLanguage Support
Object AdapterHolds a reference to the adaptee (composition)All languages
Class AdapterInherits from both Target and Adaptee (multiple inheritance)C++, Python

Use Object Adapter in most cases — it's more flexible and works in all languages. Class Adapter is only possible with multiple inheritance (Python supports it, Java and TypeScript don't).


TypeScript Implementation

// Target interface — what our code expects
interface PaymentProcessor {
  charge(amount: number, currency: string): PaymentResult;
}
 
interface PaymentResult {
  success: boolean;
  transactionId: string;
}
 
// Adaptee — third-party SDK we can't modify
class StripeAPI {
  createCharge(
    amountInCents: number,
    cur: string,
    idempotencyKey: string
  ): { id: string; status: string } {
    console.log(`Stripe: charging ${amountInCents} cents (${cur})`);
    return { id: `stripe_${Date.now()}`, status: "succeeded" };
  }
}
 
// Adapter — bridges the gap
class StripeAdapter implements PaymentProcessor {
  constructor(private stripe: StripeAPI) {}
 
  charge(amount: number, currency: string): PaymentResult {
    const amountInCents = Math.round(amount * 100);
    const key = `idempotent_${Date.now()}_${Math.random()}`;
 
    const response = this.stripe.createCharge(amountInCents, currency, key);
 
    return {
      success: response.status === "succeeded",
      transactionId: response.id,
    };
  }
}
 
// Client code — works with any PaymentProcessor
function processCheckout(processor: PaymentProcessor, amount: number) {
  const result = processor.charge(amount, "USD");
  if (result.success) {
    console.log(`Payment successful: ${result.transactionId}`);
  }
}
 
// Usage — client never knows about Stripe
const stripeAdapter = new StripeAdapter(new StripeAPI());
processCheckout(stripeAdapter, 49.99);

Multiple Adapters for Multiple Providers

// Another adaptee — PayPal SDK
class PayPalSDK {
  makePayment(options: {
    value: number;
    currency_code: string;
  }): { orderId: string; outcome: string } {
    console.log(`PayPal: ${options.value} ${options.currency_code}`);
    return { orderId: `pp_${Date.now()}`, outcome: "COMPLETED" };
  }
}
 
// PayPal adapter
class PayPalAdapter implements PaymentProcessor {
  constructor(private paypal: PayPalSDK) {}
 
  charge(amount: number, currency: string): PaymentResult {
    const response = this.paypal.makePayment({
      value: amount,
      currency_code: currency,
    });
    return {
      success: response.outcome === "COMPLETED",
      transactionId: response.orderId,
    };
  }
}
 
// Now both providers are interchangeable
const processors: Record<string, PaymentProcessor> = {
  stripe: new StripeAdapter(new StripeAPI()),
  paypal: new PayPalAdapter(new PayPalSDK()),
};
 
function checkout(cart: Cart, providerName: string) {
  const processor = processors[providerName];
  return processor.charge(cart.total, "USD"); // ✅ clean, no conditionals
}

Python Implementation

Python supports both Object Adapter and Class Adapter (via multiple inheritance):

Object Adapter

from abc import ABC, abstractmethod
from dataclasses import dataclass
import time, random
 
# Target
class PaymentProcessor(ABC):
    @abstractmethod
    def charge(self, amount: float, currency: str) -> dict:
        pass
 
# Adaptee — third-party SDK
class StripeAPI:
    def create_charge(self, amount_in_cents: int, cur: str, idempotency_key: str) -> dict:
        print(f"Stripe: charging {amount_in_cents} cents ({cur})")
        return {"id": f"stripe_{int(time.time())}", "status": "succeeded"}
 
# Object Adapter
class StripeAdapter(PaymentProcessor):
    def __init__(self, stripe: StripeAPI):
        self._stripe = stripe
 
    def charge(self, amount: float, currency: str) -> dict:
        amount_in_cents = round(amount * 100)
        key = f"idempotent_{time.time()}_{random.random()}"
 
        response = self._stripe.create_charge(amount_in_cents, currency, key)
 
        return {
            "success": response["status"] == "succeeded",
            "transaction_id": response["id"],
        }
 
# Usage
adapter = StripeAdapter(StripeAPI())
result = adapter.charge(49.99, "USD")
print(result)  # {'success': True, 'transaction_id': 'stripe_...'}

Class Adapter (Python-only — multiple inheritance)

# Class Adapter — inherits from BOTH Target and Adaptee
class StripeClassAdapter(PaymentProcessor, StripeAPI):
    """
    Inherits PaymentProcessor (Target) and StripeAPI (Adaptee).
    Overrides charge() to delegate to inherited create_charge().
    """
 
    def charge(self, amount: float, currency: str) -> dict:
        amount_in_cents = round(amount * 100)
        key = f"idempotent_{time.time()}_{random.random()}"
 
        # Call inherited method directly — no composition needed
        response = self.create_charge(amount_in_cents, currency, key)
 
        return {
            "success": response["status"] == "succeeded",
            "transaction_id": response["id"],
        }
 
# Usage — same interface, different wiring
adapter = StripeClassAdapter()
result = adapter.charge(29.99, "EUR")

Note: Class Adapter is tighter coupled — it inherits all of StripeAPI's methods and is harder to test. Prefer Object Adapter unless you have a specific reason to inherit.


Java Implementation

// Target interface
public interface PaymentProcessor {
    PaymentResult charge(double amount, String currency);
}
 
public record PaymentResult(boolean success, String transactionId) {}
 
// Adaptee — third-party SDK
public class StripeAPI {
    public StripeResponse createCharge(int amountInCents, String cur, String idempotencyKey) {
        System.out.printf("Stripe: charging %d cents (%s)%n", amountInCents, cur);
        return new StripeResponse("stripe_" + System.currentTimeMillis(), "succeeded");
    }
}
 
public record StripeResponse(String id, String status) {}
 
// Adapter
public class StripeAdapter implements PaymentProcessor {
    private final StripeAPI stripe;
 
    public StripeAdapter(StripeAPI stripe) {
        this.stripe = stripe;
    }
 
    @Override
    public PaymentResult charge(double amount, String currency) {
        int amountInCents = (int) Math.round(amount * 100);
        String key = "idempotent_" + System.currentTimeMillis();
 
        StripeResponse response = stripe.createCharge(amountInCents, currency, key);
 
        return new PaymentResult(
            "succeeded".equals(response.status()),
            response.id()
        );
    }
}
 
// Client code
public class CheckoutService {
    private final PaymentProcessor processor;
 
    public CheckoutService(PaymentProcessor processor) {
        this.processor = processor;
    }
 
    public void checkout(double amount) {
        PaymentResult result = processor.charge(amount, "USD");
        if (result.success()) {
            System.out.println("Payment successful: " + result.transactionId());
        }
    }
}
 
// Usage
CheckoutService service = new CheckoutService(new StripeAdapter(new StripeAPI()));
service.checkout(49.99);

Real-World Adapter Examples

1. Logger Adapter

Your app uses a custom Logger interface, but you want to swap in Winston, Pino, or any other logging library:

interface Logger {
  info(message: string, context?: Record<string, unknown>): void;
  error(message: string, error?: Error): void;
  warn(message: string): void;
}
 
// Adapt Winston to your Logger interface
class WinstonAdapter implements Logger {
  constructor(private winston: WinstonLogger) {}
 
  info(message: string, context?: Record<string, unknown>): void {
    this.winston.log("info", message, { metadata: context });
  }
 
  error(message: string, error?: Error): void {
    this.winston.log("error", message, {
      stack: error?.stack,
      name: error?.name,
    });
  }
 
  warn(message: string): void {
    this.winston.log("warn", message);
  }
}
 
// Adapt Pino to the same interface
class PinoAdapter implements Logger {
  constructor(private pino: PinoLogger) {}
 
  info(message: string, context?: Record<string, unknown>): void {
    this.pino.info(context ?? {}, message);
  }
 
  error(message: string, error?: Error): void {
    this.pino.error({ err: error }, message);
  }
 
  warn(message: string): void {
    this.pino.warn(message);
  }
}

2. Data Format Adapter

Convert between XML and JSON when integrating legacy and modern APIs:

interface DataProvider {
  getData(): Record<string, unknown>;
}
 
// Legacy system returns XML
class LegacyXMLService {
  fetchData(): string {
    return "<user><name>Alice</name><age>30</age></user>";
  }
}
 
class XMLToJSONAdapter implements DataProvider {
  constructor(private xmlService: LegacyXMLService) {}
 
  getData(): Record<string, unknown> {
    const xml = this.xmlService.fetchData();
    return this.parseXML(xml);
  }
 
  private parseXML(xml: string): Record<string, unknown> {
    // simplified — in production use a library like fast-xml-parser
    const nameMatch = xml.match(/<name>(.*?)<\/name>/);
    const ageMatch = xml.match(/<age>(.*?)<\/age>/);
    return {
      name: nameMatch?.[1] ?? "",
      age: Number(ageMatch?.[1] ?? 0),
    };
  }
}

Part 2: The Facade Pattern

The Problem: Complexity Overload

You're building an e-commerce system. Placing an order involves multiple subsystems:

// ❌ Client code must orchestrate 6 different subsystems
function placeOrder(userId: string, items: CartItem[]) {
  // 1. Validate inventory
  const inventory = new InventoryService();
  for (const item of items) {
    if (!inventory.checkStock(item.productId, item.quantity)) {
      throw new Error(`Out of stock: ${item.productId}`);
    }
  }
 
  // 2. Calculate pricing
  const pricing = new PricingEngine();
  const subtotal = pricing.calculateSubtotal(items);
  const tax = pricing.calculateTax(subtotal, "US-CA");
  const discount = pricing.applyPromoCode("SAVE10", subtotal);
  const total = subtotal + tax - discount;
 
  // 3. Process payment
  const payment = new PaymentGateway();
  const paymentResult = payment.authorize(userId, total);
  if (!paymentResult.success) throw new Error("Payment failed");
  payment.capture(paymentResult.authorizationId);
 
  // 4. Reserve inventory
  for (const item of items) {
    inventory.reserve(item.productId, item.quantity);
  }
 
  // 5. Create shipping
  const shipping = new ShippingService();
  const address = new AddressBook().getDefault(userId);
  const shipment = shipping.createShipment(items, address);
  shipping.schedulePickup(shipment.id);
 
  // 6. Send notifications
  const notifier = new NotificationService();
  notifier.sendOrderConfirmation(userId, paymentResult.orderId);
  notifier.sendShippingNotification(userId, shipment.trackingNumber);
}

Problems:

  1. Every client that places an order must know the correct sequence of 6 subsystems
  2. Tight coupling — clients depend on every subsystem's API
  3. Fragile — changing any subsystem's API breaks every client
  4. Duplicated orchestration — web checkout, mobile app, API endpoint all repeat this logic

Pattern Structure

Participants:

  • Facade — provides a simple, unified interface to the subsystem
  • Subsystems — the complex classes that do the real work (unchanged)
  • Client — uses the facade instead of interacting with subsystems directly

Key distinction from Adapter: The Facade doesn't translate interfaces — it simplifies them. The subsystems don't have incompatible interfaces; they're just complex to orchestrate.


TypeScript Implementation

// Subsystem classes (unchanged — they're already correct individually)
class InventoryService {
  checkStock(productId: string, quantity: number): boolean {
    console.log(`Checking stock for ${productId}`);
    return true;
  }
 
  reserve(productId: string, quantity: number): void {
    console.log(`Reserved ${quantity}x ${productId}`);
  }
 
  release(productId: string, quantity: number): void {
    console.log(`Released ${quantity}x ${productId}`);
  }
}
 
class PricingEngine {
  calculateSubtotal(items: CartItem[]): number {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
 
  calculateTax(subtotal: number, region: string): number {
    const rates: Record<string, number> = { "US-CA": 0.0725, "US-TX": 0.0625 };
    return subtotal * (rates[region] ?? 0);
  }
 
  applyPromoCode(code: string, subtotal: number): number {
    const discounts: Record<string, number> = { SAVE10: 0.1, SAVE20: 0.2 };
    return subtotal * (discounts[code] ?? 0);
  }
}
 
class PaymentGateway {
  authorize(userId: string, amount: number): { success: boolean; authorizationId: string } {
    console.log(`Authorizing $${amount} for user ${userId}`);
    return { success: true, authorizationId: `auth_${Date.now()}` };
  }
 
  capture(authorizationId: string): void {
    console.log(`Captured payment: ${authorizationId}`);
  }
 
  refund(authorizationId: string): void {
    console.log(`Refunded: ${authorizationId}`);
  }
}
 
class ShippingService {
  createShipment(items: CartItem[], address: Address): { id: string; trackingNumber: string } {
    console.log(`Shipping ${items.length} items to ${address.city}`);
    return { id: `ship_${Date.now()}`, trackingNumber: `TRK${Date.now()}` };
  }
 
  schedulePickup(shipmentId: string): void {
    console.log(`Pickup scheduled for ${shipmentId}`);
  }
}
 
class NotificationService {
  sendOrderConfirmation(userId: string, orderId: string): void {
    console.log(`Order confirmation sent to ${userId}`);
  }
 
  sendShippingNotification(userId: string, trackingNumber: string): void {
    console.log(`Shipping notification sent to ${userId}`);
  }
}
 
// ✅ Facade — one simple method hides all the complexity
class OrderFacade {
  private inventory = new InventoryService();
  private pricing = new PricingEngine();
  private payment = new PaymentGateway();
  private shipping = new ShippingService();
  private notifications = new NotificationService();
 
  placeOrder(userId: string, items: CartItem[], promoCode?: string): OrderResult {
    // 1. Validate stock
    for (const item of items) {
      if (!this.inventory.checkStock(item.productId, item.quantity)) {
        return { success: false, error: `Out of stock: ${item.productId}` };
      }
    }
 
    // 2. Calculate total
    const subtotal = this.pricing.calculateSubtotal(items);
    const tax = this.pricing.calculateTax(subtotal, "US-CA");
    const discount = promoCode
      ? this.pricing.applyPromoCode(promoCode, subtotal)
      : 0;
    const total = subtotal + tax - discount;
 
    // 3. Process payment
    const paymentResult = this.payment.authorize(userId, total);
    if (!paymentResult.success) {
      return { success: false, error: "Payment failed" };
    }
    this.payment.capture(paymentResult.authorizationId);
 
    // 4. Reserve inventory
    for (const item of items) {
      this.inventory.reserve(item.productId, item.quantity);
    }
 
    // 5. Ship
    const address = { city: "San Francisco", zip: "94102" } as Address;
    const shipment = this.shipping.createShipment(items, address);
    this.shipping.schedulePickup(shipment.id);
 
    // 6. Notify
    this.notifications.sendOrderConfirmation(userId, paymentResult.authorizationId);
    this.notifications.sendShippingNotification(userId, shipment.trackingNumber);
 
    return {
      success: true,
      orderId: paymentResult.authorizationId,
      trackingNumber: shipment.trackingNumber,
      total,
    };
  }
}
 
// Client code — beautifully simple
const orderService = new OrderFacade();
const result = orderService.placeOrder("user_123", cartItems, "SAVE10");

Python Implementation

from dataclasses import dataclass
import time
 
# Subsystem classes
class InventoryService:
    def check_stock(self, product_id: str, quantity: int) -> bool:
        print(f"Checking stock for {product_id}")
        return True
 
    def reserve(self, product_id: str, quantity: int) -> None:
        print(f"Reserved {quantity}x {product_id}")
 
class PricingEngine:
    def calculate_subtotal(self, items: list[dict]) -> float:
        return sum(item["price"] * item["quantity"] for item in items)
 
    def calculate_tax(self, subtotal: float, region: str) -> float:
        rates = {"US-CA": 0.0725, "US-TX": 0.0625}
        return subtotal * rates.get(region, 0)
 
    def apply_promo_code(self, code: str, subtotal: float) -> float:
        discounts = {"SAVE10": 0.1, "SAVE20": 0.2}
        return subtotal * discounts.get(code, 0)
 
class PaymentGateway:
    def authorize(self, user_id: str, amount: float) -> dict:
        print(f"Authorizing ${amount} for {user_id}")
        return {"success": True, "authorization_id": f"auth_{int(time.time())}"}
 
    def capture(self, authorization_id: str) -> None:
        print(f"Captured payment: {authorization_id}")
 
class ShippingService:
    def create_shipment(self, items: list[dict], address: dict) -> dict:
        print(f"Shipping {len(items)} items to {address['city']}")
        return {"id": f"ship_{int(time.time())}", "tracking": f"TRK{int(time.time())}"}
 
    def schedule_pickup(self, shipment_id: str) -> None:
        print(f"Pickup scheduled for {shipment_id}")
 
class NotificationService:
    def send_order_confirmation(self, user_id: str, order_id: str) -> None:
        print(f"Order confirmation sent to {user_id}")
 
    def send_shipping_notification(self, user_id: str, tracking: str) -> None:
        print(f"Shipping notification sent to {user_id}")
 
 
# ✅ Facade
@dataclass
class OrderResult:
    success: bool
    order_id: str = ""
    tracking_number: str = ""
    total: float = 0.0
    error: str = ""
 
 
class OrderFacade:
    def __init__(self):
        self._inventory = InventoryService()
        self._pricing = PricingEngine()
        self._payment = PaymentGateway()
        self._shipping = ShippingService()
        self._notifications = NotificationService()
 
    def place_order(
        self, user_id: str, items: list[dict], promo_code: str | None = None
    ) -> OrderResult:
        # 1. Validate stock
        for item in items:
            if not self._inventory.check_stock(item["product_id"], item["quantity"]):
                return OrderResult(success=False, error=f"Out of stock: {item['product_id']}")
 
        # 2. Calculate total
        subtotal = self._pricing.calculate_subtotal(items)
        tax = self._pricing.calculate_tax(subtotal, "US-CA")
        discount = self._pricing.apply_promo_code(promo_code, subtotal) if promo_code else 0
        total = subtotal + tax - discount
 
        # 3. Process payment
        payment_result = self._payment.authorize(user_id, total)
        if not payment_result["success"]:
            return OrderResult(success=False, error="Payment failed")
        self._payment.capture(payment_result["authorization_id"])
 
        # 4. Reserve inventory
        for item in items:
            self._inventory.reserve(item["product_id"], item["quantity"])
 
        # 5. Ship
        shipment = self._shipping.create_shipment(items, {"city": "San Francisco"})
        self._shipping.schedule_pickup(shipment["id"])
 
        # 6. Notify
        self._notifications.send_order_confirmation(user_id, payment_result["authorization_id"])
        self._notifications.send_shipping_notification(user_id, shipment["tracking"])
 
        return OrderResult(
            success=True,
            order_id=payment_result["authorization_id"],
            tracking_number=shipment["tracking"],
            total=total,
        )
 
 
# Usage
facade = OrderFacade()
result = facade.place_order("user_123", [
    {"product_id": "SKU001", "quantity": 2, "price": 29.99},
    {"product_id": "SKU002", "quantity": 1, "price": 49.99},
], promo_code="SAVE10")
 
print(f"Order {'placed' if result.success else 'failed'}: {result}")

Java Implementation

// Subsystem classes (simplified)
public class InventoryService {
    public boolean checkStock(String productId, int quantity) {
        System.out.println("Checking stock for " + productId);
        return true;
    }
 
    public void reserve(String productId, int quantity) {
        System.out.printf("Reserved %dx %s%n", quantity, productId);
    }
}
 
public class PricingEngine {
    public double calculateSubtotal(List<CartItem> items) {
        return items.stream()
            .mapToDouble(item -> item.price() * item.quantity())
            .sum();
    }
 
    public double calculateTax(double subtotal, String region) {
        return switch (region) {
            case "US-CA" -> subtotal * 0.0725;
            case "US-TX" -> subtotal * 0.0625;
            default -> 0;
        };
    }
}
 
public class PaymentGateway {
    public PaymentAuth authorize(String userId, double amount) {
        System.out.printf("Authorizing $%.2f for %s%n", amount, userId);
        return new PaymentAuth(true, "auth_" + System.currentTimeMillis());
    }
 
    public void capture(String authorizationId) {
        System.out.println("Captured: " + authorizationId);
    }
}
 
public record PaymentAuth(boolean success, String authorizationId) {}
public record CartItem(String productId, int quantity, double price) {}
 
// ✅ Facade
public class OrderFacade {
    private final InventoryService inventory = new InventoryService();
    private final PricingEngine pricing = new PricingEngine();
    private final PaymentGateway payment = new PaymentGateway();
 
    public OrderResult placeOrder(String userId, List<CartItem> items) {
        // 1. Validate stock
        for (CartItem item : items) {
            if (!inventory.checkStock(item.productId(), item.quantity())) {
                return OrderResult.failure("Out of stock: " + item.productId());
            }
        }
 
        // 2. Calculate total
        double subtotal = pricing.calculateSubtotal(items);
        double tax = pricing.calculateTax(subtotal, "US-CA");
        double total = subtotal + tax;
 
        // 3. Process payment
        PaymentAuth auth = payment.authorize(userId, total);
        if (!auth.success()) {
            return OrderResult.failure("Payment failed");
        }
        payment.capture(auth.authorizationId());
 
        // 4. Reserve inventory
        for (CartItem item : items) {
            inventory.reserve(item.productId(), item.quantity());
        }
 
        return OrderResult.success(auth.authorizationId(), total);
    }
}
 
public record OrderResult(boolean success, String orderId, double total, String error) {
    public static OrderResult success(String orderId, double total) {
        return new OrderResult(true, orderId, total, "");
    }
 
    public static OrderResult failure(String error) {
        return new OrderResult(false, "", 0, error);
    }
}
 
// Usage
OrderFacade facade = new OrderFacade();
OrderResult result = facade.placeOrder("user_123", List.of(
    new CartItem("SKU001", 2, 29.99),
    new CartItem("SKU002", 1, 49.99)
));
System.out.println(result.success() ? "Order placed!" : "Failed: " + result.error());

Real-World Facade Examples

1. Video Conversion Facade

// Complex subsystem
class VideoFile { constructor(public path: string) {} }
class CodecFactory {
  detect(file: VideoFile): string { return "h264"; }
}
class MPEG4Codec { encode(data: Buffer): Buffer { return data; } }
class OGGCodec { encode(data: Buffer): Buffer { return data; } }
class AudioExtractor { extract(file: VideoFile): Buffer { return Buffer.from([]); } }
class AudioMixer { mix(video: Buffer, audio: Buffer): Buffer { return video; } }
class BitrateReader {
  static read(file: VideoFile): Buffer { return Buffer.from([]); }
  static convert(buffer: Buffer, codec: MPEG4Codec | OGGCodec): Buffer {
    return codec.encode(buffer);
  }
}
 
// ✅ Facade — hides codec detection, audio extraction, mixing, encoding
class VideoConverterFacade {
  convert(inputPath: string, outputFormat: string): string {
    const file = new VideoFile(inputPath);
    const sourceCodec = new CodecFactory().detect(file);
 
    const codec = outputFormat === "mp4" ? new MPEG4Codec() : new OGGCodec();
 
    const videoData = BitrateReader.read(file);
    const audioData = new AudioExtractor().extract(file);
    const convertedVideo = BitrateReader.convert(videoData, codec);
    const finalFile = new AudioMixer().mix(convertedVideo, audioData);
 
    const outputPath = inputPath.replace(/\.\w+$/, `.${outputFormat}`);
    console.log(`Converted ${inputPath} → ${outputPath}`);
    return outputPath;
  }
}
 
// Client sees one method
const converter = new VideoConverterFacade();
converter.convert("video.avi", "mp4");

2. Database Facade

// Complex subsystem: connection pooling, query building, transactions
class ConnectionPool {
  acquire(): Connection { /* ... */ return {} as Connection; }
  release(conn: Connection): void { /* ... */ }
}
 
class QueryBuilder {
  select(table: string, where?: Record<string, unknown>): string {
    const conditions = where
      ? " WHERE " + Object.entries(where).map(([k, v]) => `${k} = '${v}'`).join(" AND ")
      : "";
    return `SELECT * FROM ${table}${conditions}`;
  }
 
  insert(table: string, data: Record<string, unknown>): string {
    const cols = Object.keys(data).join(", ");
    const vals = Object.values(data).map(v => `'${v}'`).join(", ");
    return `INSERT INTO ${table} (${cols}) VALUES (${vals})`;
  }
}
 
class TransactionManager {
  begin(conn: Connection): void { /* ... */ }
  commit(conn: Connection): void { /* ... */ }
  rollback(conn: Connection): void { /* ... */ }
}
 
// ✅ Facade
class DatabaseFacade {
  private pool = new ConnectionPool();
  private queryBuilder = new QueryBuilder();
  private txManager = new TransactionManager();
 
  async findAll(table: string, where?: Record<string, unknown>): Promise<Row[]> {
    const conn = this.pool.acquire();
    try {
      const sql = this.queryBuilder.select(table, where);
      return await conn.execute(sql);
    } finally {
      this.pool.release(conn);
    }
  }
 
  async insert(table: string, data: Record<string, unknown>): Promise<void> {
    const conn = this.pool.acquire();
    this.txManager.begin(conn);
    try {
      const sql = this.queryBuilder.insert(table, data);
      await conn.execute(sql);
      this.txManager.commit(conn);
    } catch (error) {
      this.txManager.rollback(conn);
      throw error;
    } finally {
      this.pool.release(conn);
    }
  }
}
 
// Client — no knowledge of pools, builders, transactions
const db = new DatabaseFacade();
const users = await db.findAll("users", { role: "admin" });
await db.insert("users", { name: "Alice", role: "user" });

Adapter vs Facade: When to Use Which

AspectAdapterFacade
GoalMake an incompatible interface compatibleSimplify a complex subsystem
Number of classes wrappedUsually oneMultiple
Interface changeConverts interface A to interface BCreates a new simplified interface
Client awarenessClient doesn't know about the adapteeClient doesn't know about subsystem internals
When to useIntegrating third-party or legacy codeReducing coupling to complex internals
SOLID principleDependency InversionInterface Segregation

Combining Both Patterns

In practice, you often use both together. The Facade orchestrates the workflow, and Adapters handle incompatible third-party interfaces within that workflow:

// Adapters for incompatible third-party services
const stripeAdapter = new StripeAdapter(new StripeAPI());
const sendgridAdapter = new SendGridAdapter(new SendGridAPI());
 
// Facade uses adapters as its subsystems
class OrderFacade {
  constructor(
    private payment: PaymentProcessor,       // adapter
    private emailer: EmailService,           // adapter
    private inventory: InventoryService      // our own code
  ) {}
 
  placeOrder(userId: string, items: CartItem[]): OrderResult {
    // Facade orchestrates; adapters handle interface translation
    this.inventory.reserve(items);
    const payment = this.payment.charge(calculateTotal(items), "USD");
    this.emailer.sendConfirmation(userId, payment.transactionId);
    return { success: true };
  }
}

Common Pitfalls

1. Creating a "God Adapter" That Does Too Much

// ❌ Adapter doing business logic — it should only translate
class StripeAdapter implements PaymentProcessor {
  charge(amount: number, currency: string): PaymentResult {
    if (amount > 10000) throw new Error("Amount too high"); // ❌ business rule
    if (currency !== "USD") amount *= getExchangeRate(currency); // ❌ business logic
 
    const response = this.stripe.createCharge(/* ... */);
    return { success: response.status === "succeeded", /* ... */ };
  }
}
 
// ✅ Adapter only translates — business logic belongs elsewhere
class StripeAdapter implements PaymentProcessor {
  charge(amount: number, currency: string): PaymentResult {
    const amountInCents = Math.round(amount * 100);
    const response = this.stripe.createCharge(amountInCents, currency, generateKey());
    return {
      success: response.status === "succeeded",
      transactionId: response.id,
    };
  }
}

2. Facade That Exposes Subsystem Details

// ❌ Leaking subsystem types through the facade
class OrderFacade {
  placeOrder(userId: string, items: CartItem[]): {
    payment: StripeResponse; // ❌ leaks Stripe type
    shipment: FedExShipment; // ❌ leaks FedEx type
  } {
    // ...
  }
}
 
// ✅ Facade defines its own return types
class OrderFacade {
  placeOrder(userId: string, items: CartItem[]): OrderResult {
    // facade-defined type — no subsystem leakage
  }
}

3. Making Everything a Facade

// ❌ Unnecessary facade — just wrapping one thing with no simplification
class UserFacade {
  getUser(id: string) {
    return this.userRepository.findById(id); // just delegating — adds nothing
  }
}
 
// ✅ Only use facade when there's genuine complexity to hide
// A single repository call doesn't need a facade

4. Adapter With Wrong Direction

// ❌ Adapting OUR code to match the third-party interface — backwards!
class OurPaymentAdapter implements StripeInterface {
  // Don't adapt your code to third-party APIs
}
 
// ✅ Adapt the THIRD-PARTY code to match OUR interface
class StripeAdapter implements OurPaymentProcessor {
  // Third-party adapts to us — we own the interface
}

Summary

The Adapter and Facade are the two most common structural patterns. They both wrap existing code, but for different reasons:

  • Adapter converts an incompatible interface into one you expect — it's a translator
  • Facade simplifies a complex subsystem into one easy-to-use interface — it's a simplifier

Key takeaways:

✅ Use Adapter when integrating third-party libraries, legacy systems, or any code whose interface doesn't match yours
✅ Use Facade when a subsystem has too many classes and the client needs a simpler API
✅ Prefer Object Adapter (composition) over Class Adapter (inheritance) — it's more flexible and works in all languages
✅ An Adapter should only translate — keep business logic out of it
✅ A Facade should hide subsystem details — don't leak internal types through the facade's API
✅ You can combine both: Facade orchestrates, Adapters translate within the workflow


What's Next

Up next in the OOP & Design Patterns series:

  • OOP-13: Decorator and Proxy Patterns — wrapping objects to extend or control behavior
  • OOP-14: Strategy and Template Method Patterns — behavioral patterns for interchangeable algorithms
  • OOP-15: Observer, Command, and State Patterns — behavioral patterns for event handling and state machines

Practice Exercises

  1. Multi-Cloud Storage Adapter: Create a StorageService interface with upload(key, data), download(key), and delete(key) methods. Write adapters for AWS S3 (which uses putObject/getObject/deleteObject) and Google Cloud Storage (which uses bucket.file.save()/bucket.file.download()/bucket.file.delete()). Write a test that swaps providers without changing any client code.

  2. Home Automation Facade: Build a SmartHomeFacade with methods like goodMorning(), leaveHome(), movieMode(), and goodNight(). Each method orchestrates multiple subsystems: lights (brightness, color temperature), thermostat (temperature, mode), security (arm/disarm, cameras), and entertainment (TV, speakers). Implement in TypeScript with at least 4 subsystem classes.

  3. Legacy API Migration: You have a legacy REST client that returns XML strings. Build an adapter that makes it conform to a modern ApiClient interface returning typed objects. Then build a facade that coordinates multiple adapted API calls into a single getDashboardData() method.

  4. Adapter + Facade Combined: Build an email notification system with a NotificationFacade.notify(userId, event) that orchestrates user preference lookup, template rendering, and email sending. The email sending should use an adapter pattern so you can swap between SendGrid, Mailgun, and a console logger for testing.


Part 12 of the OOP & Design Patterns series. First structural pattern post, building on Prototype Pattern.

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.