Episode 9 — System Design / 9.4 — Structural Design Patterns

9.4 -- Structural Design Patterns: Quick Revision

Compact cheat sheet. Print-friendly.

How to use this material (instructions)

  1. Skim top-to-bottom in one pass before quizzes or interviews.
  2. If a row feels fuzzy -- open the matching lesson: README.md -> 9.4.a...9.4.e.
  3. Deep practice -- 9.4-Exercise-Questions.md.
  4. Polish phrasing -- 9.4-Interview-Questions.md.

Master Comparison Table

AdapterFacadeProxyDecoratorComposite
One-linerTranslates interface A to BSimplifies complex subsystemControls access to objectAdds behavior dynamicallyUniform tree structure
Wraps1 object (different interface)Many objects (subsystem)1 object (same interface)1 object (same interface)Many objects (tree)
Changes interface?Yes (translates)Yes (simplifies)NoNoNo
Adds behavior?NoNoSometimes (caching, logging)Yes (primary purpose)No (enables recursion)
Stackable?RarelyNoSometimesYes (key feature)Yes (tree nesting)
SOLID principleOpen/ClosedSingle ResponsibilityOpen/ClosedOpen/ClosedOpen/Closed

Pattern One-Liners (for interviews)

Pattern"Elevator pitch"
Adapter"Makes incompatible interfaces work together by wrapping one in a translation layer."
Facade"Provides a simplified interface to a complex subsystem so callers do not need to know the internals."
Proxy"Places a surrogate in front of an object to control access -- same interface, added control."
Decorator"Wraps an object to add behavior without modifying it -- like stacking middleware in Express."
Composite"Treats individual objects and groups uniformly via a tree structure with recursive operations."

When-to-Use Quick Guide

  PROBLEM                                  PATTERN
  =======================================  ==========
  Third-party API has wrong shape     -->  ADAPTER
  Multiple vendors, need one interface-->  ADAPTER
  Legacy system integration           -->  ADAPTER

  Complex multi-step workflow         -->  FACADE
  API Gateway for microservices       -->  FACADE
  Reduce coupling to subsystem        -->  FACADE

  Lazy loading expensive object       -->  PROXY (virtual)
  Access control / permissions        -->  PROXY (protection)
  Cache results transparently         -->  PROXY (caching)
  Audit trail / request logging       -->  PROXY (logging)

  Add logging without modifying code  -->  DECORATOR
  Middleware pipeline (Express)       -->  DECORATOR
  Combine optional behaviors freely   -->  DECORATOR
  Retry, timeout, rate-limit wrappers -->  DECORATOR

  File system (files + folders)       -->  COMPOSITE
  Menu with submenus                  -->  COMPOSITE
  Org chart (employees + departments) -->  COMPOSITE
  Nested permissions / groups         -->  COMPOSITE

Adapter -- Key Points

  Client ----> Adapter ----> Adaptee
  (our code)   (translates)  (third-party)
// Adapter translates processPayment() to stripe.charges.create()
class StripeAdapter {
  constructor(apiKey) {
    this.stripe = new StripeSDK(apiKey);
  }
  async processPayment(amount, token) {
    const result = await this.stripe.createCharge(
      Math.round(amount * 100), 'usd', token
    );
    return { id: result.id, status: 'success', amount: result.amount / 100 };
  }
}
  • Object adapter (composition) preferred over class adapter (inheritance)
  • Adapter does NOT add behavior -- only translates
  • One adapter per vendor; client depends on the abstract interface

Facade -- Key Points

  Client ----> Facade ----> SubsystemA
                      ----> SubsystemB
                      ----> SubsystemC
class EmailFacade {
  constructor(smtpConfig) {
    this.template = new TemplateEngine();
    this.validator = new EmailValidator();
    this.smtp = new SmtpClient(smtpConfig);
    this.queue = new EmailQueue();
  }
  sendWelcomeEmail(user) {
    this.validator.validate(user.email);
    const { subject, body } = this.template.compile('welcome', { name: user.name });
    this.smtp.send({ to: user.email, subject, body });
  }
}
  • Facade delegates, does NOT implement subsystem logic
  • Clients can still bypass facade for fine-grained control
  • Watch for God Object: keep each facade focused on one domain

Proxy -- Key Points

  Client ----> Proxy ----> Real Object
  (same interface)
TypeWhat it doesExample
VirtualDefers creation until first useImage lazy loading
ProtectionChecks permissions before forwardingDocument access control
CachingStores and returns cached resultsDatabase query cache
LoggingRecords every accessAPI request audit trail
// Virtual proxy -- lazy loading
class ImageProxy {
  constructor(filename) {
    this.filename = filename;
    this._real = null;
  }
  display() {
    if (!this._real) this._real = new HeavyImage(this.filename);
    this._real.display();
  }
}

// JavaScript built-in Proxy
const validated = new Proxy(user, {
  set(target, prop, value) {
    if (prop === 'age' && (value < 0 || value > 150)) throw new Error('Invalid');
    target[prop] = value;
    return true;
  }
});

Decorator -- Key Points

  Client ----> DecoratorB ----> DecoratorA ----> Component
  (each has same interface, each adds behavior)
// Function composition approach (idiomatic JS)
function withLogging(fn) {
  return async (...args) => {
    console.log(`Calling ${fn.name}`, args);
    const result = await fn(...args);
    console.log(`Result:`, result);
    return result;
  };
}

function withCache(fn, ttl = 5000) {
  const cache = new Map();
  return async (...args) => {
    const key = JSON.stringify(args);
    const hit = cache.get(key);
    if (hit && Date.now() - hit.time < ttl) return hit.data;
    const result = await fn(...args);
    cache.set(key, { data: result, time: Date.now() });
    return result;
  };
}

function withRetry(fn, max = 3) {
  return async (...args) => {
    for (let i = 1; i <= max; i++) {
      try { return await fn(...args); }
      catch (e) { if (i === max) throw e; }
    }
  };
}

// Compose: logging( cache( retry( fetchUser ) ) )
const enhanced = withLogging(withCache(withRetry(fetchUser)));
  • Express middleware = decorator pattern
  • Order matters: logging(cache(fn)) logs cache hits; cache(logging(fn)) does not
  • Higher-order functions are the JS-native way to do decorators

Composite -- Key Points

  [Root Composite]
  /       |        \
  [Composite] [Leaf]  [Composite]
  /    \              /    \
  [Leaf] [Leaf]   [Leaf] [Leaf]
class File {
  constructor(name, size) { this.name = name; this.size = size; }
  getSize() { return this.size; }
  display(indent = '') { console.log(`${indent}- ${this.name} (${this.size}B)`); }
}

class Directory {
  constructor(name) { this.name = name; this.children = []; }
  add(child) { this.children.push(child); return this; }
  getSize() {
    return this.children.reduce((sum, c) => sum + c.getSize(), 0); // RECURSIVE
  }
  display(indent = '') {
    console.log(`${indent}[${this.name}/]`);
    this.children.forEach(c => c.display(indent + '  '));
  }
}
  • Three roles: Component (interface), Leaf (end node), Composite (container)
  • All nodes respond to the same methods -- client never type-checks
  • Power is in recursive operations: getSize, display, search, count, validate

Confused Pairs -- Quick Distinctions

ComparisonKey difference
Adapter vs FacadeAdapter translates ONE interface; Facade simplifies a WHOLE subsystem
Proxy vs DecoratorProxy controls ACCESS; Decorator adds BEHAVIOR
Adapter vs ProxyAdapter changes the interface; Proxy keeps the SAME interface
Decorator vs CompositeDecorator wraps one object in layers; Composite builds a TREE of objects
Facade vs ProxyFacade simplifies multiple objects; Proxy controls access to ONE object

The Wrapper Family at a Glance

  Pattern     Wraps to...        Interface    Stackable?
  =========   =================  ===========  ==========
  ADAPTER     TRANSLATE          Different    No
  FACADE      SIMPLIFY           Different    No
  PROXY       CONTROL ACCESS     Same         Sometimes
  DECORATOR   ADD BEHAVIOR       Same         Yes (key feature)

Real-World Implementations

PatternJavaScript / Node.js example
AdapterDatabase drivers (pg, mysql2) adapting vendor protocols to a common query interface
FacadeAWS SDK methods that orchestrate multiple API calls; Express app.listen() hiding HTTP server setup
ProxyJavaScript Proxy object; Vue.js reactivity system; ORM lazy loading of relations
DecoratorExpress/Koa middleware; TypeScript decorators (@Injectable, @Controller); higher-order components in React
CompositeReact component tree; DOM (Document Object Model); file system APIs (fs recursive operations)

<- Back to 9.4 -- Structural Design Patterns (README)