Design Patterns — Rules
1. Philosophy
- Patterns are vocabulary — shared language for recurring solutions, not rigid templates.
- Prefer composition — object composition over class inheritance.
- SOLID first — patterns emerge from SOLID principles, not replace them.
- YAGNI — apply patterns when complexity demands, not preemptively.
- JavaScript/TypeScript native — leverage ESM, closures, functions before classic patterns.
2. Essential Creational Patterns
Factory Method
// factories/userFactory.ts
interface User {
id: string;
email: string;
role: "admin" | "user" | "guest";
}
type UserFactory = (data: Partial<User>) => User;
export const createAdmin: UserFactory = (data) => ({
id: crypto.randomUUID(),
email: data.email,
role: "admin",
});
export const createUser: UserFactory = (data) => ({
id: crypto.randomUUID(),
email: data.email,
role: "user",
});
export const createGuest: UserFactory = () => ({
id: crypto.randomUUID(),
email: `guest-${Date.now()}@example.com`,
role: "guest",
});
Builder (for complex objects)
// builders/queryBuilder.ts
class QueryBuilder<T> {
private filters: Record<string, unknown> = {};
private sorts: string[] = [];
private page = 1;
private limit = 20;
where(key: string, value: unknown): this {
this.filters[key] = value;
return this;
}
orderBy(field: string, dir: "asc" | "desc" = "asc"): this {
this.sorts.push(`${field}:${dir}`);
return this;
}
paginate(page: number, limit = 20): this {
this.page = page;
this.limit = limit;
return this;
}
build(): QueryOptions<T> {
return {
filters: this.filters,
sorts: this.sorts,
page: this.page,
limit: this.limit,
};
}
}
// Usage
const query = new QueryBuilder<User>()
.where("status", "active")
.orderBy("createdAt", "desc")
.paginate(1, 50)
.build();
Singleton (Module-level, not class)
// services/config.ts
const config = {
apiUrl: process.env.API_URL || "http://localhost:3000",
timeout: 5000,
retries: 3,
} as const;
export const configService = {
get: <K extends keyof typeof config>(key: K) => config[key],
set: <K extends keyof typeof config>(key: K, value: (typeof config)[K]) => {
config[key] = value;
},
} as const;
Rules
- Factory — when object creation logic is complex or varies
- Builder — when object has many optional parameters
- Singleton — prefer module-level constants over class singletons
- Avoid Abstract Factory — rarely needed in JS/TS
3. Essential Structural Patterns
Adapter
// adapters/paymentAdapter.ts
interface PaymentGateway {
charge(amount: number, currency: string): Promise<ChargeResult>;
}
class StripeAdapter implements PaymentGateway {
constructor(private stripe: StripeClient) {}
async charge(amount: number, currency: string): Promise<ChargeResult> {
const charge = await this.stripe.charges.create({ amount, currency });
return { id: charge.id, status: charge.status };
}
}
class PayPalAdapter implements PaymentGateway {
constructor(private paypal: PayPalClient) {}
async charge(amount: number, currency: string): Promise<ChargeResult> {
const order = await this.paypal.createOrder({ amount, currency });
return { id: order.id, status: order.status };
}
}
Decorator (Function Composition)
// decorators/retryDecorator.ts
function withRetry<T extends (...args: any[]) => Promise<any>>(
fn: T,
options: { retries: number; delay: number } = { retries: 3, delay: 1000 },
): T {
return (async (...args) => {
let lastError: Error;
for (let i = 0; i <= options.retries; i++) {
try {
return await fn(...args);
} catch (e) {
lastError = e as Error;
if (i < options.retries) await sleep(options.delay * 2 ** i);
}
}
throw lastError!;
}) as T;
}
// Usage
const fetchWithRetry = withRetry(fetchUser, { retries: 3, delay: 1000 });
Proxy (Lazy Loading / Access Control)
// proxies/userProxy.ts
function createUserProxy(user: User): User {
return new Proxy(user, {
get(target, prop) {
if (prop === "ssn") {
throw new Error("Access denied: SSN is protected");
}
return Reflect.get(target, prop);
},
set(target, prop, value) {
if (prop === "id") throw new Error("Cannot modify ID");
return Reflect.set(target, prop, value);
},
});
}
Rules Essential Structural Patterns
- Adapter — when integrating incompatible interfaces
- Decorator — cross-cutting concerns (logging, retry, caching)
- Proxy — access control, lazy loading, validation
- Avoid Facade/Composite/Bridge — rarely needed in modern JS/TS
4. Essential Behavioral Patterns
Strategy
// strategies/pricingStrategy.ts
interface PricingStrategy {
calculate(basePrice: number, context: PricingContext): Money;
}
class StandardPricing implements PricingStrategy {
calculate(base: Money, ctx: PricingContext): Money {
return ctx.isMember ? base.multiply(0.9) : base;
}
}
class PromotionalPricing implements PricingStrategy {
calculate(base: Money, ctx: PricingContext): Money {
const discount = ctx.promoCode ? 0.2 : 0;
return base.multiply(1 - discount);
}
}
class PricingService {
constructor(private strategy: PricingStrategy) {}
setStrategy(strategy: PricingStrategy): void {
this.strategy = strategy;
}
calculatePrice(base: Money, ctx: PricingContext): Money {
return this.strategy.calculate(base, ctx);
}
}
Observer (Event Emitter)
// events/eventEmitter.ts
type EventHandler<T> = (data: T) => void | Promise<void>;
class EventEmitter<T extends Record<string, any>> {
private handlers: Map<keyof T, Set<EventHandler<any>>> = new Map();
on<K extends keyof T>(event: K, handler: EventHandler<T[K]>): () => void {
const handlers = this.handlers.get(event) || new Set();
handlers.add(handler);
this.handlers.set(event, handlers);
return () => handlers.delete(handler);
}
async emit<K extends keyof T>(event: K, data: T[K]): Promise<void> {
const handlers = this.handlers.get(event) || new Set();
await Promise.all([...handlers].map((h) => h(data)));
}
}
// Usage
const events = new EventEmitter<{ userCreated: User; orderPlaced: Order }>();
const unsubscribe = events.on("userCreated", async (user) => {
await emailService.sendWelcome(user.email);
});
Command
// commands/command.ts
interface Command {
execute(): Promise<void>;
undo(): Promise<void>;
}
class CreateOrderCommand implements Command {
constructor(
private orderService: OrderService,
private data: CreateOrderData,
) {}
async execute(): Promise<void> {
this.orderId = await this.orderService.create(this.data);
}
async undo(): Promise<void> {
await this.orderService.cancel(this.orderId!);
}
}
// Invoker
class CommandInvoker {
private history: Command[] = [];
async execute(command: Command): Promise<void> {
await command.execute();
this.history.push(command);
}
async undo(): Promise<void> {
const command = this.history.pop();
if (command) await command.undo();
}
}
Rules Essential Behavioral Patterns
- Strategy — when algorithm varies at runtime
- Observer — decoupled event notification
- Command — undo/redo, queuing, logging
- Avoid State/Template Method/Visitor — rarely needed in functional JS/TS
5. Module Pattern (ESM Native)
// modules/cache.ts
const cache = new Map<string, { value: any; expiry: number }>();
export function set(key: string, value: any, ttlMs = 60000): void {
cache.set(key, { value, expiry: Date.now() + ttlMs });
}
export function get<T>(key: string): T | undefined {
const entry = cache.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiry) {
cache.delete(key);
return undefined;
}
return entry.value as T;
}
export function clear(): void {
cache.clear();
}
// Usage
import { set, get } from "./cache.js";
set("key", { data: "value" }, 30000);
const value = get("key");
Rules ESM Native
- ESM is the module system — no IIFE/revealing module pattern needed
- Named exports — prefer over default
- Pure functions — no hidden state in modules
- Tree-shakeable — side-effect free imports
6. Composition over Inheritance
Reference: see
component-designandclean-codeskills.
Principle
// ❌ Inheritance (rigid)
class Animal {
speak() {
return "sound";
}
}
class Dog extends Animal {
speak() {
return "woof";
}
}
// ✅ Composition (flexible)
interface Speaker {
speak(): string;
}
const dogSpeaker: Speaker = { speak: () => "woof" };
const catSpeaker: Speaker = { speak: () => "meow" };
function makeSound(speaker: Speaker) {
console.log(speaker.speak());
}
Rules Composition on Inheritance
- Interfaces over abstract classes
- Function composition —
pipe(f, g)(x)=g(f(x)) - Mixins via functions —
withLogging(component)notclass extends LoggingMixin
7. Inversion of Control / Dependency Injection
// di/container.ts
interface ServiceRegistry {
register<T>(token: string, factory: () => T): void;
resolve<T>(token: string): T;
}
class Container implements ServiceRegistry {
private services = new Map<string, { factory: () => any; instance?: any }>();
private singletons = new Set<string>();
register<T>(token: string, factory: () => T, singleton = false): void {
this.services.set(token, { factory });
if (singleton) this.singletons.add(token);
}
resolve<T>(token: string): T {
const service = this.services.get(token);
if (!service) throw new Error(`Service ${token} not registered`);
if (this.singletons.has(token)) {
if (!service.instance) service.instance = service.factory();
return service.instance;
}
return service.factory();
}
}
// Usage
const container = new Container();
container.register("userRepo", () => new PrismaUserRepository(), true);
container.register("emailService", () => new SendGridEmailService());
// In handler
const userRepo = container.resolve<UserRepository>("userRepo");
Rules Inversion of Control
- Explicit registration — no magic auto-wiring
- Singletons opt-in — most services are transient
- Constructor injection — preferred over property/method injection
- No container in domain — only in composition root
8. Methodology
Before using ANY design pattern not documented in this skill:
- MCP Context7 (priority):
context7_resolve-library-id+context7_query-docsfor design patterns, TypeScript. - Official docs: Design Patterns (GoF), Refactoring to Patterns (Kerievsky) — verify current relevance.
- Project config:
package.json,tsconfig.json— verify against actual setup. - HARD RULE: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.
9. Prohibitions
- ❌ Do not use Singleton class — module-level is sufficient
- ❌ Do not use Abstract Factory — rarely needed
- ❌ Do not use inheritance for code reuse — composition
- ❌ Do not create God objects — single responsibility
- ❌ Do not use patterns as checkboxes — solve problems, not apply patterns
- ❌ Do not use Observer for simple callbacks — use functions
- ❌ Do not use Visitor — pattern matching / discriminated unions instead
- ❌ Do not use Template Method — higher-order functions instead
10. References
Note: For JavaScript conventions, see JavaScript Note: For TypeScript rules, see TypeScript Note: For Component patterns, see Component Design Note: For Clean Code, see Clean Code Note: For DDD patterns, see DDD
Last updated: 2026-08