MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Design Patterns in JavaScript: The Essential Guide

Implement Gang of Four patterns in JavaScript: Singleton, Observer, Factory, Strategy.

JavaScriptDesign PatternsArchitectureOOP

By MinhVo

Introduction

Design patterns are reusable solutions to common problems in software design. The Gang of Four (GoF) patterns, cataloged in 1994 by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, remain the foundation of object-oriented software architecture. While the original patterns were described in C++ and Smalltalk, JavaScript's dynamic nature—prototypal inheritance, first-class functions, closures—makes many patterns simpler and more elegant than their classical counterparts.

This guide covers the four most essential pattern categories—Creational, Structural, and Behavioral—with practical JavaScript implementations. Each pattern includes real-world use cases, TypeScript type annotations, and guidance on when to use (and when to avoid) each pattern.

Software architecture patterns

Understanding Design Patterns: Core Concepts

Why Patterns Matter

Design patterns serve three purposes:

  1. Communication: Saying "use an Observer" conveys more information than describing the entire implementation
  2. Proven solutions: Patterns have been refined over decades across thousands of codebases
  3. Architecture: Patterns provide structure that makes codebases maintainable and extensible

JavaScript's unique features make pattern implementation particularly interesting:

  • Closures replace private variables
  • First-class functions simplify Strategy and Command patterns
  • Prototypal inheritance offers alternatives to classical inheritance
  • Modules provide namespace isolation
  • Proxy enables powerful metaprogramming

Pattern Categories

The GoF organized 23 patterns into three categories:

  • Creational: How objects are instantiated (Singleton, Factory, Builder, Prototype)
  • Structural: How objects are composed (Adapter, Decorator, Facade, Proxy)
  • Behavioral: How objects communicate (Observer, Strategy, Command, Iterator)

Pattern categories diagram

Architecture and Design Patterns

Singleton Pattern

The Singleton ensures a class has only one instance and provides a global point of access. In JavaScript, closures provide a natural implementation:

// Classic Singleton with closure
class DatabaseConnection {
  private static instance: DatabaseConnection | null = null;
  private connectionString: string;
 
  private constructor(connectionString: string) {
    this.connectionString = connectionString;
  }
 
  static getInstance(connectionString?: string): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      if (!connectionString) {
        throw new Error("Connection string required for first initialization");
      }
      DatabaseConnection.instance = new DatabaseConnection(connectionString);
    }
    return DatabaseConnection.instance;
  }
 
  query(sql: string): Promise<unknown[]> {
    console.log(`Executing: ${sql} on ${this.connectionString}`);
    return Promise.resolve([]);
  }
}
 
// Usage
const db = DatabaseConnection.getInstance("postgres://localhost/mydb");
const db2 = DatabaseConnection.getInstance(); // Returns same instance
console.log(db === db2); // true
// Module-based Singleton (more idiomatic in JavaScript)
// db.ts
let instance: DatabaseConnection | null = null;
 
export function getDatabase(): DatabaseConnection {
  if (!instance) {
    instance = new DatabaseConnection(process.env.DATABASE_URL!);
  }
  return instance;
}
 
// Usage
import { getDatabase } from "./db.ts";
const db = getDatabase();

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects. When one object changes state, all dependents are notified. This is the foundation of event-driven programming:

// Type-safe Observer implementation
type EventHandler<T = unknown> = (data: T) => void;
 
class EventEmitter<Events extends Record<string, unknown>> {
  private handlers = new Map<keyof Events, Set<EventHandler>>();
 
  on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler as EventHandler);
 
    // Return unsubscribe function
    return () => {
      this.handlers.get(event)?.delete(handler as EventHandler);
    };
  }
 
  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    this.handlers.get(event)?.forEach((handler) => handler(data));
  }
 
  once<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void {
    const unsubscribe = this.on(event, (data) => {
      unsubscribe();
      handler(data);
    });
    return unsubscribe;
  }
}
 
// Usage with typed events
interface AppEvents {
  userLogin: { userId: string; timestamp: number };
  userLogout: { userId: string };
  pageView: { path: string; referrer: string };
}
 
const events = new EventEmitter<AppEvents>();
 
const unsubscribe = events.on("userLogin", (data) => {
  console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});
 
events.emit("userLogin", { userId: "123", timestamp: Date.now() });
unsubscribe(); // Clean up

Factory Pattern

The Factory pattern creates objects without specifying their exact class. JavaScript's dynamic typing makes this pattern particularly natural:

// Abstract Factory for different notification channels
interface Notification {
  send(message: string): Promise<void>;
}
 
class EmailNotification implements Notification {
  constructor(private to: string) {}
 
  async send(message: string): Promise<void> {
    console.log(`Email to ${this.to}: ${message}`);
  }
}
 
class SlackNotification implements Notification {
  constructor(private channel: string) {}
 
  async send(message: string): Promise<void> {
    console.log(`Slack #${this.channel}: ${message}`);
  }
}
 
class SMSNotification implements Notification {
  constructor(private phone: string) {}
 
  async send(message: string): Promise<void> {
    console.log(`SMS to ${this.phone}: ${message}`);
  }
}
 
// Factory
class NotificationFactory {
  static create(type: string, recipient: string): Notification {
    switch (type) {
      case "email":
        return new EmailNotification(recipient);
      case "slack":
        return new SlackNotification(recipient);
      case "sms":
        return new SMSNotification(recipient);
      default:
        throw new Error(`Unknown notification type: ${type}`);
    }
  }
}
 
// Usage
const notification = NotificationFactory.create("email", "user@example.com");
await notification.send("Your order has shipped!");

Strategy Pattern

The Strategy pattern defines a family of algorithms and makes them interchangeable. JavaScript's first-class functions make this pattern trivially simple:

// Strategy pattern for data validation
type ValidationStrategy<T> = (value: T) => string | null;
 
const strategies: Record<string, ValidationStrategy<string>> = {
  email: (value) => {
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return "Invalid email";
    return null;
  },
  phone: (value) => {
    if (!/^\+?[\d\s-]{10,}$/.test(value)) return "Invalid phone number";
    return null;
  },
  url: (value) => {
    try {
      new URL(value);
      return null;
    } catch {
      return "Invalid URL";
    }
  },
  minLength: (value) => {
    if (value.length < 3) return "Too short";
    return null;
  },
};
 
// Validator using strategies
class Validator {
  private rules = new Map<string, ValidationStrategy<string>>();
 
  addRule(field: string, strategy: ValidationStrategy<string>): this {
    this.rules.set(field, strategy);
    return this;
  }
 
  validate(data: Record<string, string>): Map<string, string> {
    const errors = new Map<string, string>();
    for (const [field, strategy] of this.rules) {
      const error = strategy(data[field] ?? "");
      if (error) errors.set(field, error);
    }
    return errors;
  }
}
 
// Usage
const validator = new Validator()
  .addRule("email", strategies.email)
  .addRule("phone", strategies.phone);
 
const errors = validator.validate({
  email: "invalid",
  phone: "+1234567890",
});
console.log(errors); // Map { "email" => "Invalid email" }

Step-by-Step Implementation

Building a Plugin System with Observer + Factory

// Plugin architecture combining patterns
interface Plugin {
  name: string;
  version: string;
  initialize(app: App): void;
  destroy(): void;
}
 
class App {
  private plugins = new Map<string, Plugin>();
  private events = new EventEmitter<Record<string, unknown>>();
 
  register(plugin: Plugin): void {
    if (this.plugins.has(plugin.name)) {
      throw new Error(`Plugin ${plugin.name} already registered`);
    }
    plugin.initialize(this);
    this.plugins.set(plugin.name, plugin);
    this.events.emit("plugin:registered", { name: plugin.name });
  }
 
  unregister(name: string): void {
    const plugin = this.plugins.get(name);
    if (plugin) {
      plugin.destroy();
      this.plugins.delete(name);
      this.events.emit("plugin:unregistered", { name });
    }
  }
 
  on(event: string, handler: (data: unknown) => void): () => void {
    return this.events.on(event, handler);
  }
 
  emit(event: string, data: unknown): void {
    this.events.emit(event, data);
  }
}
 
// Example plugin
class LoggingPlugin implements Plugin {
  name = "logging";
  version = "1.0.0";
 
  initialize(app: App): void {
    app.on("plugin:registered", (data) => {
      console.log(`[LOG] Plugin registered: ${(data as { name: string }).name}`);
    });
  }
 
  destroy(): void {
    console.log("[LOG] Logging plugin destroyed");
  }
}
 
// Usage
const app = new App();
app.register(new LoggingPlugin());
app.register(new AnalyticsPlugin());

Implementing Decorator Pattern

// Decorator pattern for extending function behavior
type AnyFunction = (...args: unknown[]) => unknown;
 
function withLogging<T extends AnyFunction>(fn: T): T {
  return ((...args: unknown[]) => {
    console.log(`Calling ${fn.name} with args:`, args);
    const result = fn(...args);
    console.log(`${fn.name} returned:`, result);
    return result;
  }) as T;
}
 
function withRetry<T extends AnyFunction>(fn: T, maxRetries = 3): T {
  return (async (...args: unknown[]) => {
    for (let i = 0; i <= maxRetries; i++) {
      try {
        return await fn(...args);
      } catch (err) {
        if (i === maxRetries) throw err;
        console.log(`Retry ${i + 1}/${maxRetries} for ${fn.name}`);
      }
    }
  }) as T;
}
 
function withCache<T extends AnyFunction>(fn: T): T {
  const cache = new Map<string, unknown>();
 
  return ((...args: unknown[]) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  }) as T;
}
 
// Compose decorators
const fetchUser = withRetry(
  withLogging(
    withCache(async (id: string) => {
      const res = await fetch(`/api/users/${id}`);
      return res.json();
    })
  )
);

Command Pattern

// Command pattern for undo/redo functionality
interface Command {
  execute(): void;
  undo(): void;
  description: string;
}
 
class TextEditor {
  private content = "";
  private history: Command[] = [];
  private redoStack: Command[] = [];
 
  executeCommand(command: Command): void {
    command.execute();
    this.history.push(command);
    this.redoStack = []; // Clear redo stack on new action
  }
 
  undo(): void {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.redoStack.push(command);
    }
  }
 
  redo(): void {
    const command = this.redoStack.pop();
    if (command) {
      command.execute();
      this.history.push(command);
    }
  }
 
  getContent(): string {
    return this.content;
  }
 
  setContent(content: string): void {
    this.content = content;
  }
}
 
class InsertTextCommand implements Command {
  description: string;
 
  constructor(
    private editor: TextEditor,
    private text: string,
    private position: number
  ) {
    this.description = `Insert "${text}" at position ${position}`;
  }
 
  execute(): void {
    const content = this.editor.getContent();
    this.editor.setContent(
      content.slice(0, this.position) + this.text + content.slice(this.position)
    );
  }
 
  undo(): void {
    const content = this.editor.getContent();
    this.editor.setContent(
      content.slice(0, this.position) + content.slice(this.position + this.text.length)
    );
  }
}
 
// Usage
const editor = new TextEditor();
editor.executeCommand(new InsertTextCommand(editor, "Hello", 0));
editor.executeCommand(new InsertTextCommand(editor, " World", 5));
console.log(editor.getContent()); // "Hello World"
 
editor.undo();
console.log(editor.getContent()); // "Hello"
 
editor.redo();
console.log(editor.getContent()); // "Hello World"

Design patterns in practice

Real-World Use Cases

Use Case 1: State Management with Observer

// Redux-like store using Observer pattern
type Reducer<T> = (state: T, action: { type: string; payload?: unknown }) => T;
 
class Store<T> {
  private state: T;
  private listeners = new Set<(state: T) => void>();
 
  constructor(private reducer: Reducer<T>, initialState: T) {
    this.state = initialState;
  }
 
  getState(): T {
    return this.state;
  }
 
  dispatch(action: { type: string; payload?: unknown }): void {
    this.state = this.reducer(this.state, action);
    this.listeners.forEach((listener) => listener(this.state));
  }
 
  subscribe(listener: (state: T) => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
}
 
// Usage
interface AppState {
  count: number;
  todos: string[];
}
 
const reducer: Reducer<AppState> = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "ADD_TODO":
      return { ...state, todos: [...state.todos, action.payload as string] };
    default:
      return state;
  }
};
 
const store = new Store(reducer, { count: 0, todos: [] });
 
store.subscribe((state) => {
  console.log("State changed:", state);
});
 
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "ADD_TODO", payload: "Learn patterns" });

Use Case 2: API Client with Strategy + Decorator

// Flexible API client using multiple patterns
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
 
interface RequestConfig {
  method: HttpMethod;
  url: string;
  data?: unknown;
  headers?: Record<string, string>;
}
 
type RequestInterceptor = (config: RequestConfig) => RequestConfig;
type ResponseInterceptor = (response: Response) => Response;
 
class ApiClient {
  private requestInterceptors: RequestInterceptor[] = [];
  private responseInterceptors: ResponseInterceptor[] = [];
 
  addRequestInterceptor(interceptor: RequestInterceptor): void {
    this.requestInterceptors.push(interceptor);
  }
 
  addResponseInterceptor(interceptor: ResponseInterceptor): void {
    this.responseInterceptors.push(interceptor);
  }
 
  async request(config: RequestConfig): Promise<unknown> {
    // Apply request interceptors (Chain of Responsibility pattern)
    let processedConfig = config;
    for (const interceptor of this.requestInterceptors) {
      processedConfig = interceptor(processedConfig);
    }
 
    let response = await fetch(processedConfig.url, {
      method: processedConfig.method,
      headers: processedConfig.headers,
      body: processedConfig.data ? JSON.stringify(processedConfig.data) : undefined,
    });
 
    // Apply response interceptors
    for (const interceptor of this.responseInterceptors) {
      response = interceptor(response);
    }
 
    return response.json();
  }
}
 
// Usage
const api = new ApiClient();
 
api.addRequestInterceptor((config) => ({
  ...config,
  headers: {
    ...config.headers,
    Authorization: `Bearer ${getToken()}`,
  },
}));
 
api.addResponseInterceptor((response) => {
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response;
});

Use Case 3: Form Validation Chain

// Chain of Responsibility for form validation
interface ValidatorRule {
  validate(value: unknown): string | null;
}
 
class RequiredRule implements ValidatorRule {
  validate(value: unknown): string | null {
    if (value === undefined || value === null || value === "") {
      return "This field is required";
    }
    return null;
  }
}
 
class EmailRule implements ValidatorRule {
  validate(value: unknown): string | null {
    if (typeof value === "string" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return "Invalid email address";
    }
    return null;
  }
}
 
class MinLengthRule implements ValidatorRule {
  constructor(private min: number) {}
 
  validate(value: unknown): string | null {
    if (typeof value === "string" && value.length < this.min) {
      return `Minimum ${this.min} characters required`;
    }
    return null;
  }
}
 
class ValidationChain {
  private rules = new Map<string, ValidatorRule[]>();
 
  addField(field: string, ...rules: ValidatorRule[]): this {
    if (!this.rules.has(field)) {
      this.rules.set(field, []);
    }
    this.rules.get(field)!.push(...rules);
    return this;
  }
 
  validate(data: Record<string, unknown>): Map<string, string[]> {
    const errors = new Map<string, string[]>();
 
    for (const [field, rules] of this.rules) {
      const fieldErrors: string[] = [];
      for (const rule of rules) {
        const error = rule.validate(data[field]);
        if (error) fieldErrors.push(error);
      }
      if (fieldErrors.length > 0) {
        errors.set(field, fieldErrors);
      }
    }
 
    return errors;
  }
}
 
// Usage
const validator = new ValidationChain()
  .addField("email", new RequiredRule(), new EmailRule())
  .addField("password", new RequiredRule(), new MinLengthRule(8));
 
const errors = validator.validate({ email: "invalid", password: "123" });
// Map { "email" => ["Invalid email address"], "password" => ["Minimum 8 characters required"] }

Best Practices for Production

  1. Don't force patterns: If a simple function solves the problem, don't create a class hierarchy. Patterns should simplify code, not add complexity.

  2. Prefer composition over inheritance: JavaScript's dynamic nature makes composition patterns (Strategy, Decorator, Observer) more flexible than inheritance-based patterns.

  3. Use TypeScript for pattern safety: Type annotations make patterns self-documenting and catch misuse at compile time.

  4. Keep patterns simple: A Singleton that's just a module-scoped object is better than a class with static methods and lazy initialization.

  5. Document pattern usage: When using a pattern, add a comment explaining which pattern and why. Future maintainers will thank you.

  6. Test patterns in isolation: Each pattern should have dedicated tests that verify its contract independent of business logic.

  7. Use established libraries: Don't implement Observer from scratch when EventEmitter exists. Use libraries like RxJS for complex reactive patterns.

  8. Consider functional alternatives: Many GoF patterns have functional equivalents that are simpler in JavaScript—closures for encapsulation, higher-order functions for Strategy, event emitters for Observer.

Common Pitfalls and Solutions

PitfallImpactSolution
Over-engineering with patternsUnnecessary complexityUse patterns only when they solve a real problem
Singleton as global stateHidden dependencies, hard to testUse dependency injection; pass dependencies explicitly
Observer memory leaksGrowing memory usageAlways unsubscribe when components unmount
Factory returning wrong typeRuntime type errorsUse TypeScript generics to ensure type safety
Strategy with too many algorithmsDecision fatigueGroup related strategies; use configuration objects
Pattern over-abstractionIndirection without valueIf a pattern doesn't simplify code, remove it

Performance Optimization

// Use WeakMap for pattern internals to prevent memory leaks
const privateData = new WeakMap<object, Map<string, unknown>>();
 
class SecureSingleton {
  private static instance: SecureSingleton | null = null;
 
  private constructor() {
    privateData.set(this, new Map());
  }
 
  static getInstance(): SecureSingleton {
    if (!SecureSingleton.instance) {
      SecureSingleton.instance = new SecureSingleton();
    }
    return SecureSingleton.instance;
  }
 
  set(key: string, value: unknown): void {
    privateData.get(this)!.set(key, value);
  }
 
  get(key: string): unknown {
    return privateData.get(this)!.get(key);
  }
}
 
// WeakMap allows garbage collection when the singleton is no longer referenced
// Optimize Observer with batched notifications
class BatchedEventEmitter {
  private pending = new Map<string, unknown[]>();
  private scheduled = false;
 
  emit(event: string, data: unknown): void {
    if (!this.pending.has(event)) {
      this.pending.set(event, []);
    }
    this.pending.get(event)!.push(data);
 
    if (!this.scheduled) {
      this.scheduled = true;
      queueMicrotask(() => this.flush());
    }
  }
 
  private flush(): void {
    for (const [event, data] of this.pending) {
      // Notify all listeners once with batched data
      this.notifyListeners(event, data);
    }
    this.pending.clear();
    this.scheduled = false;
  }
}

Comparison with Alternatives

PatternClassical ImplementationJavaScript IdiomWhen to Use
SingletonStatic classModule-scope objectDatabase connections, config
ObserverInterface + ListEventEmitterEvent-driven architecture
FactoryAbstract classFunction returning objectsObject creation varies
StrategyInterface hierarchyFunction referencesAlgorithm selection
DecoratorWrapper classesHigher-order functionsExtending behavior
CommandCommand interfaceFunction objectsUndo/redo, queuing
ProxyProxy classES6 ProxyValidation, caching

Advanced Patterns

ES6 Proxy for AOP

// Aspect-Oriented Programming with Proxy
function withAspect<T extends object>(
  target: T,
  aspects: {
    before?: (method: string, args: unknown[]) => void;
    after?: (method: string, args: unknown[], result: unknown) => void;
    onError?: (method: string, args: unknown[], error: Error) => void;
  }
): T {
  return new Proxy(target, {
    get(obj, prop) {
      if (typeof obj[prop as keyof T] === "function") {
        return (...args: unknown[]) => {
          aspects.before?.(prop as string, args);
          try {
            const result = (obj[prop as keyof T] as Function).apply(obj, args);
            if (result instanceof Promise) {
              return result
                .then((res) => {
                  aspects.after?.(prop as string, args, res);
                  return res;
                })
                .catch((err) => {
                  aspects.onError?.(prop as string, args, err);
                  throw err;
                });
            }
            aspects.after?.(prop as string, args, result);
            return result;
          } catch (err) {
            aspects.onError?.(prop as string, args, err as Error);
            throw err;
          }
        };
      }
      return obj[prop as keyof T];
    },
  });
}
 
// Usage
const service = withAspect(new UserService(), {
  before: (method, args) => console.log(`→ ${method}(${args})`),
  after: (method, args, result) => console.log(`← ${method}: ${result}`),
  onError: (method, args, err) => console.error(`✗ ${method}: ${err.message}`),
});

Testing Strategies

// Test patterns with focused unit tests
import { assertEquals, assertThrows } from "https://deno.land/std/assert/mod.ts";
 
Deno.test("Singleton returns same instance", () => {
  const a = DatabaseConnection.getInstance("postgres://localhost/test");
  const b = DatabaseConnection.getInstance();
  assertEquals(a, b);
});
 
Deno.test("Observer notifies subscribers", () => {
  const emitter = new EventEmitter();
  let received: unknown = null;
 
  emitter.on("test", (data) => { received = data; });
  emitter.emit("test", { value: 42 });
 
  assertEquals(received, { value: 42 });
});
 
Deno.test("Strategy pattern validates correctly", () => {
  const validator = new Validator().addRule("email", strategies.email);
 
  const valid = validator.validate({ email: "test@example.com" });
  assertEquals(valid.size, 0);
 
  const invalid = validator.validate({ email: "invalid" });
  assertEquals(invalid.get("email"), "Invalid email");
});
 
Deno.test("Command pattern supports undo", () => {
  const editor = new TextEditor();
  editor.executeCommand(new InsertTextCommand(editor, "Hello", 0));
  assertEquals(editor.getContent(), "Hello");
 
  editor.undo();
  assertEquals(editor.getContent(), "");
});

Future Outlook

Design patterns in JavaScript continue to evolve:

  • Reactive patterns with RxJS and Signals are replacing traditional Observer
  • Functional patterns (monads, lenses, transducers) are gaining adoption
  • Proxy-based metaprogramming enables new pattern implementations
  • Module federation creates new architectural patterns for micro-frontends

The core patterns remain relevant, but their implementations continue to become more idiomatic and expressive as JavaScript evolves.

Conclusion

Design patterns are the shared vocabulary of software engineering. In JavaScript, the language's dynamic nature—closures, first-class functions, prototypal inheritance—makes many patterns simpler and more powerful than their classical counterparts.

Key takeaways:

  1. Patterns are tools, not rules — use them when they solve a real problem
  2. JavaScript's closures replace many GoF patterns — a closure is often simpler than a class
  3. Composition beats inheritance — Strategy and Decorator are more flexible than Template Method
  4. TypeScript adds safety — type annotations make patterns self-documenting
  5. Test patterns independently — each pattern should have its own test suite

Master these patterns and you'll have the vocabulary and tools to design maintainable, extensible software systems in JavaScript.