Library
Learning Patterns · 5 of 12
Learning Patterns
UI/UX Design HIGH

Design Patterns Catalog (Modern JS)

Learning Patterns Lydia Hallie & Addy Osmani
design-patterns singleton proxy observer module factory command

Key Principle

Classical design patterns were invented for languages lacking first-class functions, closures, and module systems. Modern JavaScript has all three. The question for each pattern is not "how do I implement it?" but "does the language already solve this problem natively?" Most patterns either collapse into a language feature (Module, Singleton, Prototype) or shift into a library concern (Observer, Mediator). A few remain useful when applied sparingly (Proxy, Command, Flyweight).

The Catalog

Pattern Modern JS Verdict When to Use What Replaced It
Singleton AVOID Almost never in application code. ES modules already evaluate once and share exports by reference. Object.freeze() on a plain module-scoped object. React state tools (Redux, Context) for shared state with controlled mutation.
Proxy USE (carefully) Validation, logging, access control, or formatting on property access/set — where the interception must be transparent to callers. Nothing — Proxy is a native ES6 feature. Avoid in performance-critical paths; handler traps fire on every access.
Prototype ADAPT Understanding class/extends behavior and debugging inheritance chains. Rarely used directly. ES6 class syntax (which is sugar over prototype delegation).
Observer ADAPT Event-driven architectures, pub/sub systems, reactive data flows. RxJS (Observer + Iterator + FP operators) for complex stream composition. Native EventEmitter in Node. Framework reactivity (React state, Vue reactivity).
Module USE (it is the language) Always. ES2015 modules are the default encapsulation boundary — private by default, explicit export/import. Replaced IIFEs, revealing module pattern, and global-scope scripts.
Mixin AVOID Rarely justified. Prototype mutation causes pollution and origin ambiguity that compound with scale. React Hooks, higher-order components, or plain composition functions.
Mediator USE When N-to-N component communication creates unmanageable coupling. Express.js middleware is a mediator. No single replacement — the hub-and-spoke topology remains valid.
Flyweight USE (niche) Large collections of objects with significant shared (intrinsic) state. Memory savings = 1 - (unique types / total instances). No replacement. Factory + Map cache is the standard JS implementation.
Factory USE Object creation that involves caching, conditional logic, or hiding new. Flyweight depends on it. Arrow functions returning object literals serve as lightweight factories.
Command ADAPT (limited) Operations that need queuing, scheduling, undo/redo, or lifespan tracking. Plain functions and closures handle most dispatch needs without the indirection.

Why This Matters

Applying a pattern where the language already provides the solution adds boilerplate without benefit. The Singleton is the clearest example: class-based instantiation guards solve a problem that ES module evaluation semantics already solve. Conversely, ignoring a pattern where it fits (Flyweight for large datasets, Mediator for complex routing) means reinventing its solution ad hoc and worse.

The recurring theme across all patterns: controlled mutation and explicit coupling beat uncontrolled shared state. Proxy enforces write-time validation. Observer decouples producers from consumers at runtime. Module scoping prevents global pollution. The mechanism differs; the goal is identical.

Key Quotes

  • "Testing code that relies on a Singleton can get tricky. Since we can't create new instances each time, all tests rely on the modification to the global instance of the previous test." (Chapter 1: Singleton Pattern)
  • "Although the downsides to having a global state don't magically disappear by using these tools, we can at least make sure that the global state is mutated the way we intend it, since components cannot update the state directly." (Proxy Pattern)
  • "Overusing the Proxy object or performing heavy operations on each handler method invocation can easily affect the performance of your application negatively. It's best to not use proxies for performance-critical code." (Proxy Pattern)
  • "The value of proto on any instance of the constructor, is a direct reference to the constructor's prototype!" (Chapter 6: Prototype Pattern)
  • "By keeping the value private to the module, there is a reduced risk of accidentally polluting the global scope." (Module Pattern)
  • "Modifying an object's prototype is seen as bad practice, as it can lead to prototype pollution and a level of uncertainty regarding the origin of our functions." (Mixin Pattern)
  • "Instead of letting every objects talk directly to the other objects, resulting in a many-to-many relationship, the object's requests get handled by the mediator." (Mediator/Middleware Pattern)
  • "The flyweight pattern is a useful way to conserve memory when we're creating a large number of similar objects." (Flyweight Pattern)
  • "The use cases for the command pattern are quite limited, and often adds unnecessary boilerplate to an application." (Command Pattern)

Rules of Thumb

  1. If ES modules solve it, do not add a pattern on top. Singleton and Module Pattern both reduce to "use ES module semantics."
  2. Proxy is powerful but has a per-access cost. Never wrap hot-path objects. Use for configuration, form validation, or debug instrumentation.
  3. Observer scales linearly. Every notify() iterates all subscribers. Complex observers on a high-frequency observable become a bottleneck.
  4. Mixins mutate prototypes; prefer composition. Hooks and plain functions compose without polluting shared prototype chains.
  5. Flyweight only pays off at scale. If object count is low or intrinsic overlap is minimal, the factory-cache overhead exceeds the memory savings.
  6. Command needs a reason beyond dispatch. Without queuing, scheduling, or undo requirements, the indirection is pure boilerplate.
  7. Mediator vs. Observer: Observer = receivers self-register; Mediator = hub decides routing. Choose Mediator when routing logic is complex.

Related References

  • Singleton implementation details and ES module singleton semantics
  • Proxy with Reflect API symmetry for clean trap delegation
  • Observer-to-RxJS pipeline (Observer + Iterator + FP composition)
  • Prototype chain mechanics underlying class/extends syntax
  • Module Pattern as the foundation for all modern JS encapsulation