Design Patterns
01 / 03

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 — 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');

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free