All topics
General · Learning hub

Design Patterns notes for developers

Master Design Patterns with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore General notes
Design Patterns

Creational & Structural Patterns

Creational & Structural Patterns Singleton Ensure a class has only one instance and provide a global access point. // Module-level singleton (TypeScript/Node.js

Creational & Structural Patterns

Singleton

Ensure a class has only one instance and provide a global access point.

// Module-level singleton (TypeScript/Node.js — common pattern)
class DatabaseConnection {
  private static instance: DatabaseConnection | null = null;
  private constructor(private url: string) {}

  static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection(process.env.DB_URL!);
    }
    return DatabaseConnection.instance;
  }

  query(sql: string) { /* ... */ }
}

const db = DatabaseConnection.getInstance();

Factory Method

Define an interface for creating objects, but let subclasses decide which class to instantiate.

interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(msg: string) { console.log(msg); }
}
class FileLogger implements Logger {
  log(msg: string) { fs.appendFileSync('app.log', msg + '
'); }
}

function createLogger(type: 'console' | 'file'): Logger {
  const loggers = { console: ConsoleLogger, file: FileLogger };
  return new loggers[type]();
}

const logger = createLogger('file');
logger.log('Hello');

Builder

Construct complex objects step by step, allowing different representations.

class QueryBuilder {
  private table = '';
  private conditions: string[] = [];
  private columns = '*';
  private limitVal?: number;

  from(table: string) { this.table = table; return this; }
  select(...cols: string[]) { this.columns = cols.join(', '); return this; }
  where(condition: string) { this.conditions.push(condition); return this; }
  limit(n: number) { this.limitVal = n; return this; }

  build(): string {
    let q = `SELECT ${this.columns} FROM ${this.table}`;
    if (this.conditions.length) q += ` WHERE ${this.conditions.join(' AND ')}`;
    if (this.limitVal) q += ` LIMIT ${this.limitVal}`;
    return q;
  }
}

const query = new QueryBuilder()
  .from('users')
  .select('id', 'name', 'email')
  .where('active = true')
  .where('role = "admin"')
  .limit(10)
  .build();

Adapter

Allow incompatible interfaces to work together by wrapping one in an adapter.

// Legacy payment processor
class StripeV1 {
  chargeCard(cardNumber: string, amount: number): void { /* ... */ }
}

// New interface
interface PaymentProcessor {
  processPayment(amount: number, card: { number: string }): void;
}

// Adapter wraps legacy code to match new interface
class StripeAdapter implements PaymentProcessor {
  constructor(private stripe: StripeV1) {}

  processPayment(amount: number, card: { number: string }) {
    this.stripe.chargeCard(card.number, amount);
  }
}

const processor: PaymentProcessor = new StripeAdapter(new StripeV1());
processor.processPayment(100, { number: '4242...' });

Decorator

Attach additional behaviors to objects dynamically by wrapping them.

interface DataSource {
  read(): string;
  write(data: string): void;
}

class FileDataSource implements DataSource {
  constructor(private filename: string) {}
  read() { return fs.readFileSync(this.filename, 'utf-8'); }
  write(data: string) { fs.writeFileSync(this.filename, data); }
}

// Decorator adds compression transparently
class CompressionDecorator implements DataSource {
  constructor(private wrapped: DataSource) {}
  read() { return decompress(this.wrapped.read()); }
  write(data: string) { this.wrapped.write(compress(data)); }
}

class EncryptionDecorator implements DataSource {
  constructor(private wrapped: DataSource) {}
  read() { return decrypt(this.wrapped.read()); }
  write(data: string) { this.wrapped.write(encrypt(data)); }
}

// Stack decorators
const source = new EncryptionDecorator(
  new CompressionDecorator(
    new FileDataSource('data.bin')
  )
);
source.write('sensitive data');
Design Patterns

Behavioral Patterns

Behavioral Patterns Observer Define a one-to-many dependency so when one object changes state, all dependents are notified. interface Observer<T> { update(data:

Behavioral Patterns

Observer

Define a one-to-many dependency so when one object changes state, all dependents are notified.

interface Observer<T> {
  update(data: T): void;
}

class EventEmitter<T> {
  private observers: Observer<T>[] = [];
  subscribe(obs: Observer<T>) { this.observers.push(obs); }
  unsubscribe(obs: Observer<T>) {
    this.observers = this.observers.filter(o => o !== obs);
  }
  emit(data: T) { this.observers.forEach(o => o.update(data)); }
}

// Usage
const orderEvents = new EventEmitter<{ orderId: string; status: string }>();
orderEvents.subscribe({ update: ({ orderId, status }) => sendEmail(orderId, status) });
orderEvents.subscribe({ update: ({ orderId }) => updateAnalytics(orderId) });

orderEvents.emit({ orderId: '123', status: 'shipped' });

Strategy

Define a family of algorithms, encapsulate each one, and make them interchangeable.

type SortStrategy<T> = (arr: T[]) => T[];

const bubbleSort: SortStrategy<number> = (arr) => { /* ... */ return arr; };
const quickSort: SortStrategy<number> = (arr) => { /* ... */ return arr; };
const mergeSort: SortStrategy<number> = (arr) => { /* ... */ return arr; };

class Sorter<T> {
  constructor(private strategy: SortStrategy<T>) {}
  setStrategy(s: SortStrategy<T>) { this.strategy = s; }
  sort(arr: T[]) { return this.strategy([...arr]); }
}

const sorter = new Sorter<number>(quickSort);
sorter.sort([3, 1, 4, 1, 5]);
sorter.setStrategy(mergeSort);  // swap algorithm at runtime

Command

Encapsulate a request as an object, enabling undo/redo, queuing, and logging.

interface Command {
  execute(): void;
  undo(): void;
}

class TextEditor {
  private history: Command[] = [];
  private text = '';

  executeCommand(cmd: Command) {
    cmd.execute();
    this.history.push(cmd);
  }

  undoLast() { this.history.pop()?.undo(); }
  getText() { return this.text; }

  createInsertCommand(text: string, position: number): Command {
    return {
      execute: () => {
        this.text = this.text.slice(0, position) + text + this.text.slice(position);
      },
      undo: () => {
        this.text = this.text.slice(0, position) + this.text.slice(position + text.length);
      },
    };
  }
}

Repository Pattern

Abstract data access logic behind an interface, decoupling business logic from storage details.

interface UserRepository {
  findById(id: string): Promise<User | null>;
  findAll(filters?: UserFilters): Promise<User[]>;
  create(data: CreateUserDto): Promise<User>;
  update(id: string, data: Partial<User>): Promise<User>;
  delete(id: string): Promise<void>;
}

// PostgreSQL implementation
class PgUserRepository implements UserRepository {
  async findById(id: string) {
    return db.query('SELECT * FROM users WHERE id = $1', [id]);
  }
  // ...
}

// In-memory implementation (for tests)
class InMemoryUserRepository implements UserRepository {
  private users = new Map<string, User>();
  async findById(id: string) { return this.users.get(id) ?? null; }
  // ...
}

// Service only depends on interface, not implementation
class UserService {
  constructor(private repo: UserRepository) {}
  async getUser(id: string) { return this.repo.findById(id); }
}

// Swap implementations easily
const service = new UserService(new PgUserRepository());
const testService = new UserService(new InMemoryUserRepository());
Design Patterns

Interview Questions

Design Patterns Interview Questions Q: What are the three categories of design patterns? Creational — object creation (Singleton, Factory, Abstract Factory, Bui

Design Patterns Interview Questions

Q: What are the three categories of design patterns?

  • Creational — object creation (Singleton, Factory, Abstract Factory, Builder, Prototype)

  • Structural — object composition (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy)

  • Behavioral — communication between objects (Observer, Strategy, Command, Iterator, State, Chain of Responsibility)

Q: What is the difference between Observer and Pub/Sub?

Observer: subjects know their observers directly (direct coupling). Used in the same process. Pub/Sub: publishers and subscribers are completely decoupled through a message broker (they don't know about each other). Pub/Sub scales across processes and services (Redis pub/sub, message queues, EventEmitter). Observer is synchronous; pub/sub is often asynchronous.

Q: When would you use a Factory vs Builder pattern?

Use Factory when construction is simple and you just want to decouple the caller from knowing which concrete class to instantiate. Use Builder when construction is complex with many optional parameters, or when the same construction process should produce different representations. Builder also allows step-by-step construction and makes invalid states impossible.

Q: What is Dependency Injection and why is it useful?

DI is a technique where a class receives its dependencies from outside rather than creating them internally. Benefits: testability (inject mocks), loose coupling (depend on interfaces), flexibility (swap implementations). It's a form of Inversion of Control — the class doesn't control its own dependency creation. Frameworks like NestJS, Spring, and Angular have built-in DI containers.

Q: What is the SOLID principle?

  • S — Single Responsibility: a class should have one reason to change

  • O — Open/Closed: open for extension, closed for modification (add via new classes, not changing existing)

  • L — Liskov Substitution: subtypes must be substitutable for their base types

  • I — Interface Segregation: clients shouldn't depend on methods they don't use (many small interfaces > one large)

  • D — Dependency Inversion: depend on abstractions, not concretions

Keep your Design Patterns knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever