Vasu Patel logoVasu Patel
← Patterns of Good Design
All chapters in this track
  1. 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
  2. 02Chapter 2: Patterns and SOLID: How They Relate
  3. 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
  4. 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
  5. 05Chapter 5: Structural Patterns I: Adapter & Facade
  6. 06Chapter 6: Structural Patterns II: Decorator & Proxy
  7. 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
  8. 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
  9. 09Chapter 9: Behavioral Patterns II: Observer & Mediator
  10. 10Chapter 10: Behavioral Patterns III: Command & Memento
  11. 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
  12. 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
  13. 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
  14. 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
  15. 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
  16. 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
  17. 17Chapter 17: Architectural Patterns III: The Repository Pattern
  18. 18Chapter 18: Recognizing Which Pattern Fits an Interview Design Prompt

Chapter 5 of 18

Chapter 5: Structural Patterns I: Adapter & Facade

Why this matters

Structural patterns answer "how should these objects be composed?" — and Adapter and Facade are the two most common answers to a specific version of that question: how do I present a cleaner or different interface over something I don't control or don't want callers dealing with directly? They're easy to conflate because both "wrap" something, but they solve different problems.

Adapter

The problem it solves: you have an existing class with an interface that doesn't match what your code expects (often a third-party library, or legacy code you can't change), and you need them to work together without modifying either side.

Before — incompatible interfaces, forcing awkward special-casing at every call site:

// Third-party library — you cannot change this.
class LegacyXmlLogger {
  logAsXml(xmlPayload: string): void { console.log(`<log>${xmlPayload}</log>`); }
}

// Your code expects this interface everywhere:
interface Logger {
  log(message: string): void;
}

// Without an adapter, every call site needs to know about the XML format:
function processOrder(logger: LegacyXmlLogger, orderId: string): void {
  logger.logAsXml(`<message>Processing order ${orderId}</message>`); // leaks XML-specific detail everywhere
}

After — Adapter translates the interface once, in one place:

class XmlLoggerAdapter implements Logger {
  constructor(private legacyLogger: LegacyXmlLogger) {}

  log(message: string): void {
    this.legacyLogger.logAsXml(`<message>${message}</message>`); // translation happens exactly once, here
  }
}

function processOrder(logger: Logger, orderId: string): void {
  logger.log(`Processing order ${orderId}`); // no XML knowledge needed here at all
}

processOrder(new XmlLoggerAdapter(new LegacyXmlLogger()), "12345");
class LegacyXmlLogger:
    def log_as_xml(self, xml_payload: str) -> None:
        print(f"<log>{xml_payload}</log>")

class XmlLoggerAdapter:
    def __init__(self, legacy_logger: LegacyXmlLogger) -> None:
        self._legacy_logger = legacy_logger

    def log(self, message: str) -> None:
        self._legacy_logger.log_as_xml(f"<message>{message}</message>")

When to use it: integrating a third-party or legacy component whose interface you cannot (or should not) change, where the rest of your codebase should be insulated from that component's specific shape — this is Chapter 4's "Abstraction" lesson from the OOP book applied specifically at an integration boundary.

When to avoid it: if you control both sides and can simply change one to match the other, an adapter is unnecessary indirection — reserve it for boundaries you genuinely don't control.

Complexity cost: one small wrapper class per adapted interface — low cost, and usually a clear net win at real integration boundaries.

Facade

The problem it solves: a subsystem has many classes and a complex sequence of calls needed to accomplish a common task, and most callers only need a simple, high-level operation — not intimate knowledge of the subsystem's internals.

Before — callers must orchestrate a complex subsystem themselves (this is exactly the ReportGenerator "before" example from the OOP book's Abstraction chapter, restated at the subsystem level):

class InventoryChecker { hasStock(sku: string): boolean { return true; } }
class PaymentGateway { charge(amountCents: number): boolean { return true; } }
class ShippingScheduler { schedule(sku: string): void { console.log("Scheduled"); } }
class OrderNotifier { notifyCustomer(email: string): void { console.log("Notified"); } }

// Every caller has to know and correctly sequence four subsystems:
function placeOrderManually(sku: string, amountCents: number, email: string): void {
  const inventory = new InventoryChecker();
  const payment = new PaymentGateway();
  const shipping = new ShippingScheduler();
  const notifier = new OrderNotifier();

  if (inventory.hasStock(sku) && payment.charge(amountCents)) {
    shipping.schedule(sku);
    notifier.notifyCustomer(email);
  }
}

After — Facade provides one simple entry point:

class OrderFacade {
  private inventory = new InventoryChecker();
  private payment = new PaymentGateway();
  private shipping = new ShippingScheduler();
  private notifier = new OrderNotifier();

  placeOrder(sku: string, amountCents: number, email: string): boolean {
    if (!this.inventory.hasStock(sku)) return false;
    if (!this.payment.charge(amountCents)) return false;
    this.shipping.schedule(sku);
    this.notifier.notifyCustomer(email);
    return true;
  }
}

const orderFacade = new OrderFacade();
orderFacade.placeOrder("SKU-1", 2999, "customer@example.com"); // callers no longer need the subsystem details
class OrderFacade:
    def __init__(self) -> None:
        self._inventory = InventoryChecker()
        self._payment = PaymentGateway()
        self._shipping = ShippingScheduler()
        self._notifier = OrderNotifier()

    def place_order(self, sku: str, amount_cents: int, email: str) -> bool:
        if not self._inventory.has_stock(sku):
            return False
        if not self._payment.charge(amount_cents):
            return False
        self._shipping.schedule(sku)
        self._notifier.notify_customer(email)
        return True

When to use it: a subsystem has real internal complexity that most callers shouldn't need to understand, and there's a common, coherent operation worth exposing as one method — this is Single Responsibility (the Facade's one job is "orchestrate this subsystem for the common case") plus Abstraction, working together.

When to avoid it: the Facade shouldn't become the only way to use the subsystem if some callers legitimately need finer-grained control — a Facade should simplify the common case, not hide necessary flexibility for the uncommon one. Also avoid it when the "subsystem" is really just one or two classes already simple enough to use directly — wrapping them adds a layer with no real simplification.

Complexity cost: low — typically one new class — but a Facade that grows to cover every possible use case eventually re-accumulates the God Object anti-pattern (OOP book, Chapter 12) it was meant to prevent at the call-site level.

Adapter vs. Facade

[SENIOR INSIGHT] The distinction that gets confused: Adapter changes an interface to match what's expected (translation, usually one class wrapping one other class); Facade simplifies a complex interface (simplification, usually one class wrapping several others). An Adapter doesn't necessarily make anything simpler — it might even add steps — its job is compatibility. A Facade doesn't necessarily make anything compatible with a different interface — its job is hiding unnecessary complexity for the common case.

Common mistakes

  1. Using Adapter when you actually control both interfaces. If you can change the "incompatible" side directly, do that instead — an adapter for a self-inflicted mismatch is unnecessary layering.
  2. Building a Facade that becomes a second God Object by growing to expose every possible operation on the subsystem, defeating its own simplification purpose.
  3. Confusing which pattern is needed — reaching for an Adapter when the real need is simplification (Facade), or vice versa. The test: "is this about translation (Adapter) or about hiding complexity (Facade)?"
  4. Removing legitimate lower-level access when introducing a Facade, forcing advanced callers who genuinely need it to work around the Facade awkwardly instead of being able to drop to the subsystem directly when justified.

Discussion / interview questions

  1. "What's the difference between Adapter and Facade?" Strong answer: Adapter translates one interface into another expected interface (usually one-to-one, for compatibility); Facade simplifies access to a complex subsystem (usually one-to-many, for ease of use). Neither is a variant of the other.

  2. "When would you introduce an Adapter, and when would changing the underlying class directly be better?" Strong answer: Adapter when you don't control or shouldn't change the underlying interface (third-party library, legacy code); otherwise, if you control both sides, changing the mismatched side directly avoids unnecessary indirection.

  3. "What's a risk of introducing a Facade over a complex subsystem?" Strong answer: The Facade can grow to try to expose every possible operation, eventually becoming its own God Object, or it can accidentally remove legitimate low-level access that advanced callers still need.

  4. "Give an example of a real-world Adapter you've used or could imagine, outside of a toy example." Strong answer: A concrete scenario — e.g., wrapping a third-party payment SDK's specific method names/parameter shapes behind your own PaymentProcessor interface (Chapter 10 of the OOP book), so the rest of the codebase never depends on that vendor's exact API shape.

Chapter summary / cheat sheet

  • Adapter: translates an incompatible interface into the one your code expects — solves compatibility, typically one-to-one.
  • Facade: provides a simple entry point over a complex subsystem — solves ease-of-use, typically one-to-many.
  • Both are low-cost, high-value patterns at genuine boundaries (third-party integration for Adapter, real subsystem complexity for Facade) — and both can be introduced unnecessarily if that boundary/complexity isn't real.
  • Don't let a Facade become the only access path if some callers legitimately need the subsystem's finer-grained operations.