Theoretical Foundations
Welcome to the curriculum workspace. Here you will find long-form technical guidelines outlining core architectural blueprints and implementation mechanics.
Module 2: The Gang of Four (GoF) Design Patterns
PHASE 1 — MICRO-ARCHITECTURE: Design patterns are reusable solutions to recurring structural problems in software. They are the vocabulary of software architecture — when a senior engineer says "use a Strategy here" or "this needs a Factory," they are referencing this catalog. Mastering GoF patterns is the prerequisite for understanding the architectural styles in Phases 2 and 3, which apply the same structural thinking at the system level.
Introduction: Why Patterns Exist
In 1994, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — the "Gang of Four" — published Design Patterns: Elements of Reusable Object-Oriented Software. They documented 23 solutions to structural problems that appeared repeatedly across codebases in different languages, domains, and industries.
These patterns are not algorithms or frameworks. They are structural templates — proven arrangements of classes and interfaces that solve a specific kind of coupling problem. Each pattern has a name (the vocabulary), a problem (when to apply it), a solution (the structural arrangement), and consequences (the trade-offs).
The 23 patterns are organized into three categories:
[ GoF Design Patterns ]
|
+------------------+------------------+
| | |
[ Creational ] [ Structural ] [ Behavioral ]
Object creation Compose classes Algorithm &
and lifecycle into larger responsibility
management structures distribution
| | |
Singleton Adapter Strategy
Factory Method Facade Observer
Abstract Factory Proxy Command
Builder Decorator State
Prototype Bridge Template Method
Composite Chain of Responsibility
Flyweight Iterator / Mediator
Section 1: Creational Patterns
Creational patterns abstract the process of object instantiation. They control how objects are created, decoupling consumers from the concrete classes they depend on.
A. Factory Method
The Factory Method defines an interface for creating an object but lets subclasses decide which class to instantiate. The method delegates instantiation to subclasses.
The Problem
// VIOLATION: Consumer directly instantiates concrete classes
class NotificationService {
sendNotification(type: string, message: string): void {
if (type === 'email') {
const emailSender = new EmailSender(); // tight coupling
emailSender.send(message);
} else if (type === 'sms') {
const smsSender = new SmsSender(); // tight coupling
smsSender.send(message);
}
// Adding push notifications requires modifying this class (OCP violated)
}
}
The Solution
// Product interface
interface Notifier {
send(message: string): Promise<void>;
}
// Concrete products
class EmailNotifier implements Notifier {
async send(message: string): Promise<void> {
await sendGridClient.send({ to: 'user@domain.com', body: message });
}
}
class SmsNotifier implements Notifier {
async send(message: string): Promise<void> {
await twilioClient.messages.create({ body: message, to: '+15551234567' });
}
}
class PushNotifier implements Notifier {
async send(message: string): Promise<void> {
await firebaseClient.sendToDevice(this.deviceToken, { body: message });
}
}
// Creator with registry-based Factory Method
class NotificationFactory {
private registry = new Map<string, () => Notifier>([
['email', () => new EmailNotifier()],
['sms', () => new SmsNotifier()],
['push', () => new PushNotifier()],
]);
create(type: string): Notifier {
const factory = this.registry.get(type);
if (!factory) throw new Error(`Unknown notification type: ${type}`);
return factory();
}
async notify(type: string, message: string): Promise<void> {
const notifier = this.create(type);
await notifier.send(message);
}
}
classDiagram
class Notifier {
<<interface>>
+send(message) void
}
class EmailNotifier { +send(message) void }
class SmsNotifier { +send(message) void }
class PushNotifier { +send(message) void }
class NotificationFactory {
+create(type) Notifier
+notify(type, message) void
}
Notifier <|.. EmailNotifier
Notifier <|.. SmsNotifier
Notifier <|.. PushNotifier
NotificationFactory ..> Notifier
B. Abstract Factory
The Abstract Factory provides an interface for creating families of related objects without specifying their concrete classes. Where Factory Method creates one product, Abstract Factory creates a suite of products that must be used together.
// Abstract product interfaces
interface Button { render(): void; }
interface Table { render(): void; }
interface Modal { show(content: string): void; }
// Abstract factory — ensures component family consistency
interface UIComponentFactory {
createButton(): Button;
createTable(): Table;
createModal(): Modal;
}
// Concrete factories — one per theme
class DarkThemeFactory implements UIComponentFactory {
createButton(): Button { return new DarkButton(); }
createTable(): Table { return new DarkTable(); }
createModal(): Modal { return new DarkModal(); }
}
class LightThemeFactory implements UIComponentFactory {
createButton(): Button { return new LightButton(); }
createTable(): Table { return new LightTable(); }
createModal(): Modal { return new LightModal(); }
}
// Consumer: only depends on factory interface — theme injected at runtime
class Dashboard {
constructor(private factory: UIComponentFactory) {}
render(): void {
this.factory.createButton().render();
this.factory.createTable().render();
}
}
// Composition root
const theme = userPreferences.darkMode ? new DarkThemeFactory() : new LightThemeFactory();
new Dashboard(theme).render();
C. Builder
The Builder pattern separates the construction of a complex object from its representation. It allows the same construction process to create different representations. Most useful when an object requires many optional configuration parameters.
class QueryBuilder {
private table: string = '';
private conditions: string[] = [];
private columns: string[] = ['*'];
private orderByClause: string = '';
private limitClause: number | null = null;
private joins: string[] = [];
from(table: string): this { this.table = table; return this; }
select(...columns: string[]): this { this.columns = columns; return this; }
where(condition: string): this { this.conditions.push(condition); return this; }
join(table: string, on: string): this {
this.joins.push(`INNER JOIN ${table} ON ${on}`);
return this;
}
orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.orderByClause = `ORDER BY ${column} ${direction}`;
return this;
}
limit(n: number): this { this.limitClause = n; return this; }
build(): string {
if (!this.table) throw new Error('Table is required');
return [
`SELECT ${this.columns.join(', ')}`,
`FROM ${this.table}`,
...this.joins,
this.conditions.length ? `WHERE ${this.conditions.join(' AND ')}` : '',
this.orderByClause,
this.limitClause !== null ? `LIMIT ${this.limitClause}` : '',
].filter(Boolean).join(' ');
}
}
// Fluent, readable, self-documenting
const query = new QueryBuilder()
.from('orders')
.select('id', 'total', 'customer_id')
.join('customers', 'customers.id = orders.customer_id')
.where('orders.status = "completed"')
.where('orders.total > 100')
.orderBy('orders.created_at', 'DESC')
.limit(50)
.build();
D. Singleton
The Singleton ensures a class has exactly one instance and provides a global access point to it.
class DatabaseConnectionPool {
private static instance: DatabaseConnectionPool | null = null;
private readonly maxConnections = 20;
private pool: Connection[] = [];
private constructor() {
this.pool = this.initializePool();
}
static getInstance(): DatabaseConnectionPool {
if (!DatabaseConnectionPool.instance) {
DatabaseConnectionPool.instance = new DatabaseConnectionPool();
}
return DatabaseConnectionPool.instance;
}
acquire(): Connection {
const conn = this.pool.pop();
if (!conn) throw new Error('Connection pool exhausted');
return conn;
}
release(conn: Connection): void { this.pool.push(conn); }
}
Use Singletons only when: the resource is truly global (DB pool, config registry, logger) and multiple instances would create incorrect behavior. Avoid for testability — Singletons resist mocking.
E. Prototype
The Prototype creates new objects by cloning an existing instance rather than constructing from scratch. Used when object creation is expensive.
interface Cloneable<T> { clone(): T; }
class DocumentTemplate implements Cloneable<DocumentTemplate> {
constructor(
private title: string,
private sections: Section[],
private metadata: Record<string, string>
) {}
clone(): DocumentTemplate {
return new DocumentTemplate(
this.title,
this.sections.map(s => s.clone()), // Deep clone sections
{ ...this.metadata } // Shallow copy metadata
);
}
}
// Registry of pre-built templates
const templates = {
invoice: new DocumentTemplate('Invoice', [new HeaderSection(), new ItemsSection()], { type: 'financial' }),
};
// Clone instead of constructing from scratch
const myInvoice = templates.invoice.clone();
Section 2: Structural Patterns
Structural patterns define how classes and objects are composed to form larger structures. They resolve interface mismatches and allow objects to collaborate.
A. Adapter
The Adapter converts the interface of one class into an interface that clients expect.
// Target interface (what your application expects)
interface PaymentGateway {
charge(amount: number, currency: string, token: string): Promise<{ transactionId: string }>;
}
// Adaptee (legacy SDK — cannot be modified)
class LegacyStripeSDK {
chargeCard(opts: { amountInCents: number; currencyCode: string; cardToken: string }): string {
return `ch_${Date.now()}`; // Returns charge ID synchronously
}
}
// Adapter: wraps Adaptee, implements Target
class StripeAdapter implements PaymentGateway {
constructor(private stripe: LegacyStripeSDK) {}
async charge(amount: number, currency: string, token: string): Promise<{ transactionId: string }> {
const chargeId = this.stripe.chargeCard({
amountInCents: Math.round(amount * 100), // cents conversion
currencyCode: currency.toUpperCase(),
cardToken: token,
});
return { transactionId: chargeId };
}
}
// Consumer only knows PaymentGateway — SDK migration is transparent
class OrderService {
constructor(private gateway: PaymentGateway) {}
async pay(order: Order): Promise<void> {
const { transactionId } = await this.gateway.charge(order.total, 'USD', order.token);
order.markPaid(transactionId);
}
}
B. Facade
The Facade provides a simplified interface to a complex subsystem.
// Complex subsystem — many moving parts
class VideoEncoder { encode(file: File, codec: string): EncodedVideo { return {} as any; } }
class ThumbnailGen { generate(v: EncodedVideo): Thumbnail { return {} as any; } }
class CloudStorage { upload(buf: Buffer, path: string): string { return ''; } }
class CDNPurger { purge(urls: string[]): void {} }
class MetadataDB { save(meta: VideoMetadata): void {} }
// Facade: single entry point for video upload
class VideoUploadFacade {
constructor(
private encoder: VideoEncoder,
private thumbGen: ThumbnailGen,
private storage: CloudStorage,
private cdn: CDNPurger,
private db: MetadataDB,
) {}
async uploadVideo(rawFile: File, metadata: VideoMetadata): Promise<string> {
const encoded = this.encoder.encode(rawFile, 'h264');
const thumb = this.thumbGen.generate(encoded);
const videoUrl = this.storage.upload(encoded.buffer, `videos/${metadata.id}`);
const thumbUrl = this.storage.upload(thumb.buffer, `thumbnails/${metadata.id}`);
this.cdn.purge([videoUrl, thumbUrl]);
this.db.save({ ...metadata, videoUrl, thumbUrl });
return videoUrl;
}
}
// Consumer: one call instead of 6+ subsystem interactions
const url = await facade.uploadVideo(rawFile, { id: 'v123', title: 'Tutorial' });
C. Decorator
The Decorator attaches additional responsibilities to an object dynamically — a flexible alternative to subclassing.
interface DataReader { read(): string; }
class FileReader implements DataReader {
constructor(private path: string) {}
read(): string { return fs.readFileSync(this.path, 'utf8'); }
}
abstract class DataReaderDecorator implements DataReader {
constructor(protected wrapped: DataReader) {}
abstract read(): string;
}
class CompressionDecorator extends DataReaderDecorator {
read(): string {
const compressed = this.wrapped.read();
return zlib.gunzipSync(Buffer.from(compressed, 'base64')).toString();
}
}
class EncryptionDecorator extends DataReaderDecorator {
constructor(wrapped: DataReader, private key: Buffer) { super(wrapped); }
read(): string {
return decrypt(this.wrapped.read(), this.key);
}
}
// Stack decorators — each adds one responsibility
const reader = new EncryptionDecorator(
new CompressionDecorator(
new FileReader('/data/config.gz.enc')
),
encryptionKey
);
// Execution: FileReader → Decompress → Decrypt
reader.read();
D. Proxy
The Proxy provides a surrogate to control access to another object.
interface UserService {
findById(id: string): Promise<User>;
}
class DatabaseUserService implements UserService {
async findById(id: string): Promise<User> {
return prisma.user.findUnique({ where: { id } });
}
}
// Caching Proxy — transparent to consumer
class CachingUserProxy implements UserService {
private cache = new Map<string, { user: User; expiry: number }>();
private readonly TTL = 5 * 60 * 1000; // 5 min
constructor(private real: UserService) {}
async findById(id: string): Promise<User> {
const cached = this.cache.get(id);
if (cached && Date.now() < cached.expiry) return cached.user;
const user = await this.real.findById(id);
this.cache.set(id, { user, expiry: Date.now() + this.TTL });
return user;
}
}
const userService: UserService = new CachingUserProxy(new DatabaseUserService());
Section 3: Behavioral Patterns
Behavioral patterns define how objects collaborate and distribute responsibilities.
A. Strategy
The Strategy defines a family of algorithms, encapsulates each, and makes them interchangeable at runtime.
The MPC Billing Challenge
// Strategy interface
interface BillingStrategy {
calculateTotal(lineItems: LineItem[]): number;
applyDiscount(subtotal: number, customer: Customer): number;
}
// Concrete strategies
class StandardBilling implements BillingStrategy {
calculateTotal(items: LineItem[]): number {
return items.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
}
applyDiscount(subtotal: number, _customer: Customer): number {
return subtotal; // No volume discount
}
}
class EnterpriseBilling implements BillingStrategy {
calculateTotal(items: LineItem[]): number {
return items.reduce((sum, i) => {
const price = i.quantity > 100 ? i.unitPrice * 0.75 : i.unitPrice * 0.90;
return sum + price * i.quantity;
}, 0);
}
applyDiscount(subtotal: number, customer: Customer): number {
return subtotal * (1 - (customer.contractDiscount ?? 0));
}
}
// Context: swappable at runtime — no modification required
class InvoiceService {
private billing: BillingStrategy;
constructor(billing: BillingStrategy) { this.billing = billing; }
setBillingStrategy(strategy: BillingStrategy): void { this.billing = strategy; }
generateInvoice(items: LineItem[], customer: Customer): Invoice {
const subtotal = this.billing.calculateTotal(items);
const total = this.billing.applyDiscount(subtotal, customer);
return new Invoice(customer, items, subtotal, total);
}
}
classDiagram
class BillingStrategy {
<<interface>>
+calculateTotal(items) number
+applyDiscount(subtotal, customer) number
}
class StandardBilling {
+calculateTotal(items) number
+applyDiscount(subtotal, customer) number
}
class EnterpriseBilling {
+calculateTotal(items) number
+applyDiscount(subtotal, customer) number
}
class InvoiceService {
-billing: BillingStrategy
+generateInvoice(items, customer) Invoice
+setBillingStrategy(strategy) void
}
BillingStrategy <|.. StandardBilling
BillingStrategy <|.. EnterpriseBilling
InvoiceService --> BillingStrategy : swappable at runtime
B. Observer
The Observer defines a one-to-many dependency: when one object changes state, all dependents are notified.
interface OrderEventListener {
onOrderPlaced(order: Order): void;
}
class OrderEventBus {
private listeners = new Map<string, Set<OrderEventListener>>();
subscribe(event: string, listener: OrderEventListener): void {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
this.listeners.get(event)!.add(listener);
}
emit(event: string, order: Order): void {
this.listeners.get(event)?.forEach(l => l.onOrderPlaced(order));
}
}
// Concrete observers: each handles its own concern
class InventoryService implements OrderEventListener {
onOrderPlaced(order: Order): void {
order.items.forEach(item => this.decrementStock(item.productId, item.quantity));
}
}
class EmailService implements OrderEventListener {
onOrderPlaced(order: Order): void { this.sendConfirmation(order.customerEmail, order.id); }
}
class AnalyticsService implements OrderEventListener {
onOrderPlaced(order: Order): void { this.track('order_placed', { orderId: order.id }); }
}
// Wiring: decoupled from ordering logic
const bus = new OrderEventBus();
bus.subscribe('order.placed', new InventoryService());
bus.subscribe('order.placed', new EmailService());
bus.subscribe('order.placed', new AnalyticsService());
bus.emit('order.placed', newOrder); // All three notified in one call
C. Command
The Command encapsulates a request as an object, enabling queuing, logging, and undo/redo.
interface Command {
execute(): Promise<void>;
undo(): Promise<void>;
}
class TransferFundsCommand implements Command {
constructor(
private from: Account,
private to: Account,
private amount: number,
private ledger: LedgerService
) {}
async execute(): Promise<void> {
await this.ledger.debit(this.from, this.amount);
await this.ledger.credit(this.to, this.amount);
}
async undo(): Promise<void> {
await this.ledger.debit(this.to, this.amount);
await this.ledger.credit(this.from, this.amount);
}
}
class TransactionProcessor {
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();
}
}
D. State
The State pattern allows an object to alter its behavior when its internal state changes.
interface OrderState {
confirm(order: OrderContext): void;
ship(order: OrderContext): void;
cancel(order: OrderContext): void;
}
class PendingState implements OrderState {
confirm(order: OrderContext): void { order.setState(new ConfirmedState()); }
ship(_: OrderContext): void { throw new Error('Cannot ship a pending order'); }
cancel(order: OrderContext): void { order.setState(new CancelledState()); }
}
class ConfirmedState implements OrderState {
confirm(_: OrderContext): void { throw new Error('Already confirmed'); }
ship(order: OrderContext): void { order.setState(new ShippedState()); }
cancel(order: OrderContext): void { order.setState(new CancelledState()); }
}
class ShippedState implements OrderState {
confirm(_: OrderContext): void { throw new Error('Already confirmed'); }
ship(_: OrderContext): void { throw new Error('Already shipped'); }
cancel(_: OrderContext): void { throw new Error('Cannot cancel a shipped order'); }
}
class OrderContext {
private state: OrderState = new PendingState();
setState(state: OrderState): void { this.state = state; }
confirm(): void { this.state.confirm(this); }
ship(): void { this.state.ship(this); }
cancel(): void { this.state.cancel(this); }
}
stateDiagram-v2
[*] --> Pending
Pending --> Confirmed : confirm()
Pending --> Cancelled : cancel()
Confirmed --> Shipped : ship()
Confirmed --> Cancelled : cancel()
Shipped --> Delivered : deliver()
Delivered --> [*]
Cancelled --> [*]
E. Template Method
The Template Method defines the skeleton of an algorithm in a base class, deferring specific steps to subclasses.
abstract class ReportGenerator {
// Template Method: invariant algorithm structure
final generate(data: ReportData): Report {
const raw = this.fetchData(data);
const processed = this.processData(raw);
const formatted = this.formatOutput(processed);
return new Report(formatted, new Date(), this.reportType());
}
private fetchData(data: ReportData): RawData {
return dataWarehouse.query(data.query);
}
abstract processData(raw: RawData): ProcessedData;
abstract formatOutput(data: ProcessedData): string;
abstract reportType(): string;
}
class CSVReport extends ReportGenerator {
processData(raw: RawData): ProcessedData { return csvParser.parse(raw); }
formatOutput(data: ProcessedData): string { return csvSerializer.serialize(data); }
reportType(): string { return 'CSV'; }
}
F. Chain of Responsibility
Passes a request along a chain of handlers until one handles it.
abstract class RequestHandler {
private next: RequestHandler | null = null;
setNext(h: RequestHandler): RequestHandler { this.next = h; return h; }
handle(req: HttpRequest): HttpResponse | null {
return this.next?.handle(req) ?? null;
}
}
class AuthHandler extends RequestHandler {
handle(req: HttpRequest): HttpResponse | null {
if (!req.headers.authorization) return new HttpResponse(401, 'Unauthorized');
return super.handle(req);
}
}
class RateLimitHandler extends RequestHandler {
handle(req: HttpRequest): HttpResponse | null {
if (this.isExceeded(req.clientIp)) return new HttpResponse(429, 'Too Many Requests');
return super.handle(req);
}
}
class BusinessHandler extends RequestHandler {
handle(req: HttpRequest): HttpResponse | null {
return new HttpResponse(200, this.process(req));
}
}
// Wire chain
const auth = new AuthHandler();
auth.setNext(new RateLimitHandler()).setNext(new BusinessHandler());
const response = auth.handle(incomingRequest);
Section 4: Pattern Selection Guide
| Problem | Pattern |
|---|---|
| Create objects without specifying concrete classes | Factory Method |
| Create families of related objects | Abstract Factory |
| Complex object with many optional parameters | Builder |
| Single instance globally | Singleton |
| Expensive object creation | Prototype |
| Incompatible interfaces | Adapter |
| Simplify complex subsystem | Facade |
| Add behavior dynamically | Decorator |
| Control access (caching, auth) | Proxy |
| Swappable algorithms at runtime | Strategy |
| Notify multiple objects of changes | Observer |
| Encapsulate requests (undo/queue) | Command |
| Behavior based on internal state | State |
| Fixed skeleton, variable steps | Template Method |
| Pipeline of handlers | Chain of Responsibility |
Section 5: Anti-Patterns to Avoid
| Anti-Pattern | Description | Fix |
|---|---|---|
| God Object | One class knows everything | SRP — split by actor |
| Singletons Everywhere | Every service is global | Dependency Injection |
| Premature Abstraction | Interface with one implementation | Wait for a second implementation |
| Inappropriate Intimacy | Classes access each other's internals | Facade or Mediator |
Section 6: Hands-On Practice Challenge
Your Goal
Model the Strategy Billing System as a Mermaid class diagram showing:
- The
BillingStrategyinterface withcalculateTotalandapplyDiscountmethods. StandardBillingandEnterpriseBillingimplementingBillingStrategyusing<|..notation.InvoiceServicedepending onBillingStrategywith a labeled arrow:swappable at runtime.
The AI Mentor will evaluate your diagram for:
- Correct
<<interface>>notation onBillingStrategy. - Dependency arrows pointing toward the abstraction.
- Both concrete strategies connected with implementation arrows.
Bridge to Module 3: You now understand how individual classes relate to each other using structural patterns. Module 3 (Clean & Hexagonal Architecture) applies these same patterns at the architectural layer — organizing entire application tiers using the Ports & Adapters model.