singleton-pattern-typescript

star 0

TypeScript/Node singleton patterns (module instance, classic singleton class, DI singleton scope) with trade-offs on testability and hidden deps, plus safe usage guidance.

ilkeozi By ilkeozi schedule Updated 1/23/2026

name: singleton-pattern-typescript description: TypeScript/Node singleton patterns (module instance, classic singleton class, DI singleton scope) with trade-offs on testability and hidden deps, plus safe usage guidance. compatibility: Codex CLI / filesystem agents; no external tools required. metadata: author: codex version: 0.1.0

Singleton (TypeScript)

Intent

Ensure a single shared instance for a process-wide concern while making usage explicit and testable.

When to use

  • Process-wide cache or registry (metrics, tracing, feature flags).
  • Config snapshot loaded once at startup.
  • Expensive initialization that should happen only once.
  • A shared resource must be centralized to avoid duplication.
  • You need a single in-memory coordinator for the process.
  • The instance is infrastructure, not domain logic.
  • You can provide a clear reset or injection strategy for tests.

When NOT to use

  • Domain logic or business rules (avoid global state).
  • Hidden dependencies make code harder to reason about.
  • Global mutable state would leak across tests.
  • Tests require isolation or multiple instances.
  • A simple DI singleton-scope is available and cleaner.
  • The instance lifecycle depends on user/session scope.
  • You cannot define a clear ownership or reset strategy.

Recommended TS shapes

  • Module-level instance (preferred).
  • DI singleton-scope (preferred if you have a container).
  • Classic singleton class (only when needed).

Example 1: Module-level singleton (Config)

type Config = Readonly<{ env: string; apiBaseUrl: string }>;

class ConfigLoader {
  private config: Config | null = null;

  init(env: string, apiBaseUrl: string): void {
    if (this.config) return;
    this.config = Object.freeze({ env, apiBaseUrl });
  }

  get(): Config {
    if (!this.config) throw new Error("Config not initialized");
    return this.config;
  }
}

export const config = new ConfigLoader();

Example 2: Classic singleton class

class Logger {
  private static instance: Logger | null = null;

  private constructor(private readonly prefix: string) {}

  static getInstance(prefix = "app"): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger(prefix);
    }
    return Logger.instance;
  }

  log(message: string): void {
    console.log(`[${this.prefix}] ${message}`);
  }
}

const logger = Logger.getInstance("service");
logger.log("started");

Example 3: Async singleton initialization

class Client {
  constructor(public readonly baseUrl: string) {}
  async ping(): Promise<void> {
    return;
  }
}

let clientPromise: Promise<Client> | null = null;

export function getClient(): Promise<Client> {
  if (!clientPromise) {
    clientPromise = (async () => {
      const client = new Client("https://api.example.com");
      await client.ping();
      return client;
    })();
  }
  return clientPromise;
}

Testing strategy (pragmatic)

  • Prefer injecting an interface and a test double.
  • If you export a singleton, provide a test-only reset hook guarded by environment.

Common pitfalls

  • Hidden coupling through global access.
  • State leaks between tests.
  • Async initialization races without a cached promise.
  • Using singleton for domain rules or business logic.
  • Unclear lifecycle or ownership.
  • Hard-coded configuration at import time.
  • Skipping observability of shared state.
  • Overusing singleton when DI scope is enough.

Checklist for refactors

  • Define why it must be single and process-wide.
  • Prefer DI singleton scope when available.
  • Expose an interface and inject it where possible.
  • Use module-level instance for simple cases.
  • Add an explicit init and/or reset strategy.
  • Avoid static state in domain logic.
  • Ensure async init is guarded against races.
  • Add tests for singleton behavior and resets.

Output expectations

When invoked, produce:

  • The chosen singleton form and reasoning.
  • A wiring/injection plan for dependencies.
  • A test isolation approach (reset or injection).
Install via CLI
npx skills add https://github.com/ilkeozi/skills --skill singleton-pattern-typescript
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator