Architectural Design Patterns in JavaScript: Factory, Observer, Proxy & Module Internals
Design patterns are not academic — they are the vocabulary of code that survives team turnover. Factory, Singleton, Observer, and Proxy map directly to engine-level primitives, and understanding circular ESM dependency resolution turns a frustrating build error into a mechanical, solvable problem.
Architectural Design Patterns in JavaScript: Factory, Observer, Proxy & Module Internals
The runtime is not a black box — and neither are design patterns. When a colleague copies a Factory pattern from a blog post, they often don't know why it exists at the engine level. When they struggle with circular ESM imports, they don't know why one file sees undefined where a class should be. This article treats design patterns not as decorative labels but as mechanical consequences of how JavaScript's object system, module cache, and event-dispatch model work. Understanding the engine makes the patterns obvious.
§4.3.1 — ESM vs CJS module execution semantics — is fully covered in JavaScript Modules, Chunks & Dynamic Import. This article cross-references that foundation and focuses on new ground: circular dependency resolution, dynamic import() as an architectural tool, and the Observer and Proxy patterns as engine-level primitives — not framework abstractions.
1. Creational Patterns
1.1 Factory Method — Abstracting Object Instantiation
The new keyword is a protocol — it creates a new object, sets its prototype, calls the constructor, and returns it. But new hardcodes the concrete type at the call site. When the type to create is a runtime decision, new is the wrong tool.
When to use: When the concrete type depends on runtime configuration, environment variables, or feature flags. When construction has validation logic that should not be the caller's responsibility.
1.2 Singleton for Shared Configuration State
The ES Module system is a natural singleton: a module's top-level code runs exactly once, the first time it is imported. Subsequent imports in any file return the same module object from the cache — no additional evaluation.
Node.js's require() cache and the ESM module registry both implement the singleton pattern at the runtime level. A class-based Singleton in JavaScript is solving a problem the module system already solves — it adds complexity without adding a guarantee.
2. Structural and Behavioral Patterns
2.1 Observer Pattern for Custom Event Buses
The Observer pattern decouples producers (components that emit events) from consumers (components that react to events). The alternative — direct method calls — creates coupling: the producer must know which consumers exist and their method signatures.

The on() method returning an unsubscribe function is the correct Node.js-compatible pattern. Always store the return value and call it in cleanup (React's useEffect return, Svelte's onDestroy, etc.) to prevent EventEmitter memory leaks.
2.2 Proxy Pattern for Object Reactivity and Validation
The Proxy API is covered in depth in Part 5. In the context of design patterns, the key application is validation at the boundary:
3. Module Federation — Circular Dependencies & Dynamic Imports
3.1 ESM vs CJS — Execution Behavior
ESM modules are statically linked and live bindings: the import is a live reference to the exporting module's namespace object. CJS modules are synchronously evaluated and cached: require() executes the file and returns a snapshot of module.exports.
For the full treatment of this difference, see JavaScript Modules, Chunks & Dynamic Import. What matters here: this behavioral difference determines how circular dependencies behave.
3.2 Resolving Circular Dependencies
A circular dependency occurs when module A imports from module B, and module B imports from module A. In ESM, this is handled via live bindings — but there is a window during module evaluation where a binding exists but its value has not been assigned yet.
The solution is lazy resolution with dynamic import():

3.3 Dynamic Imports and Code Splitting Optimization
Dynamic import() is not just a circular-dependency fix — it is the primary mechanism for code splitting: deferring the loading of a module until the moment it is needed.
Dynamic import() is a network request at runtime. It has latency, it can fail, and the browser caches it based on HTTP headers — treat it with the same discipline as fetch(). Failing to handle the rejected Promise from a import() call is as dangerous as failing to handle a failed fetch().
Summary
| Pattern | Rule |
|---|---|
| Factory function | Use when the concrete type is a runtime decision — callers know the interface, not the class |
| Module-level singleton | ES Modules are cached after first evaluation — exported objects are singletons by default |
| Observer / Event Bus | Decouple producers from consumers — emitters know the event name and payload, not the subscribers |
| Proxy for validation | Intercept set traps to enforce invariants at the boundary — fail fast at the write site |
| Circular ESM | Static imports in circular dependencies see undefined during initialization — dynamic import() inside a function defers evaluation and breaks the cycle |
Dynamic import() |
A runtime network request — handle failures, leverage HTTP caching, prefetch on hover/intent |
What's Next
In Part 5, we go deeper into the Proxy API: all 13 trap types, the Reflect API's role in forwarding intercepted operations correctly, building a reactive state manager from scratch, and the ToPrimitive coercion algorithm that governs every
==comparison in JavaScript. Part 5 → Metaprogramming: Proxy, Reflect & Coercion
References
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.