Skip to main content

Structural Design Patterns in JavaScript

Created: May 8, 2026 Larry Qu 7 min read

Structural patterns deal with object composition and relationships. This article covers Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy patterns.

Introduction

Structural patterns provide:

  • Object composition
  • Flexible relationships
  • Simplified interfaces
  • Reduced memory usage
  • Enhanced functionality

Understanding these patterns helps you:

  • Compose objects effectively
  • Simplify complex systems
  • Adapt incompatible interfaces
  • Reduce memory footprint
  • Enhance existing objects

Adapter Pattern

Basic Adapter

// ✅ Good: Adapter pattern
class OldAPI {
  getData() {
    return { user_name: 'John', user_email: '[email protected]' };
  }
}

class NewAPI {
  getUser() {
    throw new Error('Must implement getUser');
  }
}

class APIAdapter extends NewAPI {
  constructor(oldAPI) {
    super();
    this.oldAPI = oldAPI;
  }

  getUser() {
    const data = this.oldAPI.getData();
    return {
      name: data.user_name,
      email: data.user_email
    };
  }
}

// Usage
const oldAPI = new OldAPI();
const adapter = new APIAdapter(oldAPI);
console.log(adapter.getUser());
// { name: 'John', email: '[email protected]' }

Object Adapter

// ✅ Good: Object adapter
class LegacyPaymentSystem {
  processPayment(amount) {
    console.log(`Processing payment: $${amount}`);
    return true;
  }
}

class ModernPaymentInterface {
  pay(amount) {
    throw new Error('Must implement pay');
  }
}

class PaymentAdapter {
  constructor(legacySystem) {
    this.legacySystem = legacySystem;
  }

  pay(amount) {
    return this.legacySystem.processPayment(amount);
  }
}

// Usage
const legacy = new LegacyPaymentSystem();
const adapter = new PaymentAdapter(legacy);
adapter.pay(100); // Processing payment: $100

Bridge Pattern

// ✅ Good: Bridge pattern
class Shape {
  constructor(color) {
    this.color = color;
  }

  draw() {
    throw new Error('Must implement draw');
  }
}

class Circle extends Shape {
  draw() {
    return `Drawing circle with ${this.color.getColor()}`;
  }
}

class Rectangle extends Shape {
  draw() {
    return `Drawing rectangle with ${this.color.getColor()}`;
  }
}

class Color {
  getColor() {
    throw new Error('Must implement getColor');
  }
}

class RedColor extends Color {
  getColor() {
    return 'red';
  }
}

class BlueColor extends Color {
  getColor() {
    return 'blue';
  }
}

// Usage
const redCircle = new Circle(new RedColor());
console.log(redCircle.draw()); // Drawing circle with red

const blueRectangle = new Rectangle(new BlueColor());
console.log(blueRectangle.draw()); // Drawing rectangle with blue

Composite Pattern

Basic Composite

// ✅ Good: Composite pattern
class Component {
  constructor(name) {
    this.name = name;
  }

  add(component) {
    throw new Error('Must implement add');
  }

  remove(component) {
    throw new Error('Must implement remove');
  }

  display(indent = '') {
    throw new Error('Must implement display');
  }
}

class Leaf extends Component {
  display(indent = '') {
    console.log(`${indent}${this.name}`);
  }
}

class Composite extends Component {
  constructor(name) {
    super(name);
    this.children = [];
  }

  add(component) {
    this.children.push(component);
  }

  remove(component) {
    this.children = this.children.filter(c => c !== component);
  }

  display(indent = '') {
    console.log(`${indent}${this.name}`);
    this.children.forEach(child => child.display(indent + '  '));
  }
}

// Usage
const root = new Composite('root');
const branch1 = new Composite('branch1');
const branch2 = new Composite('branch2');

root.add(branch1);
root.add(branch2);

branch1.add(new Leaf('leaf1'));
branch1.add(new Leaf('leaf2'));
branch2.add(new Leaf('leaf3'));

root.display();
// root
//   branch1
//     leaf1
//     leaf2
//   branch2
//     leaf3

File System Composite

// ✅ Good: File system using composite
class FileSystemItem {
  constructor(name) {
    this.name = name;
  }

  getSize() {
    throw new Error('Must implement getSize');
  }

  display(indent = '') {
    throw new Error('Must implement display');
  }
}

class File extends FileSystemItem {
  constructor(name, size) {
    super(name);
    this.size = size;
  }

  getSize() {
    return this.size;
  }

  display(indent = '') {
    console.log(`${indent}📄 ${this.name} (${this.size}KB)`);
  }
}

class Directory extends FileSystemItem {
  constructor(name) {
    super(name);
    this.items = [];
  }

  add(item) {
    this.items.push(item);
  }

  getSize() {
    return this.items.reduce((sum, item) => sum + item.getSize(), 0);
  }

  display(indent = '') {
    console.log(`${indent}📁 ${this.name}`);
    this.items.forEach(item => item.display(indent + '  '));
  }
}

// Usage
const root = new Directory('root');
const documents = new Directory('documents');
const images = new Directory('images');

root.add(documents);
root.add(images);

documents.add(new File('resume.pdf', 100));
documents.add(new File('cover_letter.doc', 50));
images.add(new File('photo.jpg', 500));

root.display();
console.log(`Total size: ${root.getSize()}KB`);

Decorator Pattern

Basic Decorator

// ✅ Good: Decorator pattern
class Coffee {
  cost() {
    return 5;
  }

  description() {
    return 'Coffee';
  }
}

class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost();
  }

  description() {
    return this.coffee.description();
  }
}

class Milk extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 1;
  }

  description() {
    return this.coffee.description() + ', Milk';
  }
}

class Sugar extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 0.5;
  }

  description() {
    return this.coffee.description() + ', Sugar';
  }
}

// Usage
let coffee = new Coffee();
console.log(coffee.description()); // Coffee
console.log(coffee.cost()); // 5

coffee = new Milk(coffee);
console.log(coffee.description()); // Coffee, Milk
console.log(coffee.cost()); // 6

coffee = new Sugar(coffee);
console.log(coffee.description()); // Coffee, Milk, Sugar
console.log(coffee.cost()); // 6.5

Function Decorator

// ✅ Good: Function decorator
function withLogging(fn) {
  return function(...args) {
    console.log(`Calling ${fn.name} with:`, args);
    const result = fn(...args);
    console.log(`Result:`, result);
    return result;
  };
}

function withTiming(fn) {
  return function(...args) {
    const start = performance.now();
    const result = fn(...args);
    const end = performance.now();
    console.log(`Execution time: ${end - start}ms`);
    return result;
  };
}

function add(a, b) {
  return a + b;
}

const decoratedAdd = withTiming(withLogging(add));
decoratedAdd(2, 3);
// Calling add with: [2, 3]
// Result: 5
// Execution time: 0.123ms

Facade Pattern

// ✅ Good: Facade pattern
class CPU {
  freeze() {
    console.log('CPU: Freezing');
  }

  jump(position) {
    console.log(`CPU: Jumping to ${position}`);
  }

  execute() {
    console.log('CPU: Executing');
  }
}

class Memory {
  load(position, data) {
    console.log(`Memory: Loading ${data} at ${position}`);
  }
}

class HardDrive {
  read(lba, size) {
    console.log(`HardDrive: Reading ${size} bytes from ${lba}`);
    return 'data';
  }
}

class ComputerFacade {
  constructor() {
    this.cpu = new CPU();
    this.memory = new Memory();
    this.hardDrive = new HardDrive();
  }

  start() {
    this.cpu.freeze();
    this.memory.load(0, this.hardDrive.read(0, 1024));
    this.cpu.jump(0);
    this.cpu.execute();
  }
}

// Usage
const computer = new ComputerFacade();
computer.start();
// CPU: Freezing
// Memory: Loading data at 0
// HardDrive: Reading 1024 bytes from 0
// CPU: Jumping to 0
// CPU: Executing

Flyweight Pattern

// ✅ Good: Flyweight pattern
class TreeType {
  constructor(name, color, texture) {
    this.name = name;
    this.color = color;
    this.texture = texture;
  }

  draw(canvas, x, y) {
    console.log(`Drawing ${this.name} at (${x}, ${y})`);
  }
}

class TreeTypeFactory {
  constructor() {
    this.treeTypes = {};
  }

  getTreeType(name, color, texture) {
    const key = `${name}_${color}_${texture}`;

    if (!this.treeTypes[key]) {
      this.treeTypes[key] = new TreeType(name, color, texture);
    }

    return this.treeTypes[key];
  }
}

class Tree {
  constructor(x, y, treeType) {
    this.x = x;
    this.y = y;
    this.treeType = treeType;
  }

  draw(canvas) {
    this.treeType.draw(canvas, this.x, this.y);
  }
}

// Usage
const factory = new TreeTypeFactory();

const trees = [];
for (let i = 0; i < 1000; i++) {
  const type = factory.getTreeType('Oak', 'green', 'rough');
  trees.push(new Tree(Math.random() * 100, Math.random() * 100, type));
}

console.log(`Created 1000 trees with ${Object.keys(factory.treeTypes).length} unique types`);
// Created 1000 trees with 1 unique types

Proxy Pattern

Protection Proxy

// ✅ Good: Protection proxy
class RealSubject {
  request() {
    return 'Real subject response';
  }
}

class ProtectionProxy {
  constructor(realSubject, password) {
    this.realSubject = realSubject;
    this.password = password;
  }

  request(password) {
    if (password !== this.password) {
      throw new Error('Access denied');
    }
    return this.realSubject.request();
  }
}

// Usage
const subject = new RealSubject();
const proxy = new ProtectionProxy(subject, 'secret');

console.log(proxy.request('secret')); // Real subject response
console.log(proxy.request('wrong')); // Error: Access denied

Virtual Proxy

// ✅ Good: Virtual proxy for lazy loading
class ExpensiveObject {
  constructor() {
    console.log('Creating expensive object');
    this.data = 'Important data';
  }

  getData() {
    return this.data;
  }
}

class VirtualProxy {
  constructor() {
    this.realObject = null;
  }

  getData() {
    if (!this.realObject) {
      this.realObject = new ExpensiveObject();
    }
    return this.realObject.getData();
  }
}

// Usage
const proxy = new VirtualProxy();
console.log('Proxy created');
console.log(proxy.getData()); // Creating expensive object, Important data
console.log(proxy.getData()); // Important data (no creation)

Best Practices

  1. Use Adapter for incompatible interfaces:
    // ✅ Good
    const adapter = new Adapter(legacySystem);
    
    // ❌ Bad
    const data = legacySystem.oldMethod();
    ```javascript
    
  2. Use Composite for tree structures:
    // ✅ Good
    root.add(branch);
    branch.add(leaf);
    
    // ❌ Bad
    const tree = { children: [{ children: [] }] };
    ```javascript
    
  3. Use Facade for complex subsystems:
    // ✅ Good
    const facade = new Facade();
    facade.complexOperation();
    
    // ❌ Bad
    subsystem1.operation();
    subsystem2.operation();
    subsystem3.operation();
    ```javascript
    

Common Mistakes

  1. Overusing Decorator:
    // ❌ Bad - too many decorators
    const obj = new D1(new D2(new D3(new D4(original))));
    
    // ✅ Good - use composition
    const obj = compose(original, d1, d2, d3);
    ```javascript
    
  2. Proxy without clear purpose:
    // ❌ Bad - unnecessary proxy
    const proxy = new Proxy(obj, {});
    
    // ✅ Good - proxy with purpose
    const proxy = new Proxy(obj, { get: (t, p) => { } });
    

Summary

Structural patterns organize object relationships. Key takeaways:

  • Adapter: Incompatible interfaces
  • Bridge: Abstraction and implementation
  • Composite: Tree structures
  • Decorator: Add functionality
  • Facade: Simplify complex systems
  • Flyweight: Reduce memory
  • Proxy: Control access
  • Improves flexibility
  • Reduces coupling
  • Enhances functionality

Next Steps

Resources

Comments

Share this article

Scan to read on mobile