git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/gof-singleton" ~/.claude/skills/intense-visions-harness-engineering-gof-singleton-b313f2 && rm -rf "$T"
agents/skills/codex/gof-singleton/SKILL.mdGOF Singleton
Ensure a class has exactly one instance using module-level singletons and WeakRef patterns.
When to Use
- You need a single shared instance of a resource (database connection pool, logger, config)
- You want to control access to a shared resource and lazy-initialize it
- You see repeated
calls across the codebase that all should share one instancenew Service() - NOT when you need one instance per request or per test (use dependency injection instead)
Instructions
Prefer module-level singletons over class-based singletons in TypeScript. Node.js module caching gives you singleton behavior for free.
Module-level singleton (preferred):
// db.ts — Node's module cache guarantees one instance import { Pool } from 'pg'; let pool: Pool | null = null; export function getPool(): Pool { if (!pool) { pool = new Pool({ connectionString: process.env.DATABASE_URL }); } return pool; } export async function closePool(): Promise<void> { if (pool) { await pool.end(); pool = null; } }
Class-based singleton (when interface polymorphism matters):
class Logger { private static instance: Logger | null = null; private readonly prefix: string; private constructor(prefix: string) { this.prefix = prefix; } static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger('[App]'); } return Logger.instance; } log(message: string): void { console.log(`${this.prefix} ${new Date().toISOString()} ${message}`); } } // Usage const logger = Logger.getInstance(); logger.log('Server started');
Reset for testing — always expose a reset method or use a factory:
class ConfigStore { private static instance: ConfigStore | null = null; static getInstance(): ConfigStore { if (!ConfigStore.instance) { ConfigStore.instance = new ConfigStore(); } return ConfigStore.instance; } // Only for tests — do not expose in production API static _reset(): void { ConfigStore.instance = null; } }
WeakRef variant (allows GC when no other references exist):
class ExpensiveCache { private static ref: WeakRef<ExpensiveCache> | null = null; static getInstance(): ExpensiveCache { const existing = ExpensiveCache.ref?.deref(); if (existing) return existing; const instance = new ExpensiveCache(); ExpensiveCache.ref = new WeakRef(instance); return instance; } }
Details
Why module-level beats class-based: TypeScript compiles to CommonJS or ESM. In CommonJS,
require() caches modules — the first require('./db') runs the module, subsequent calls return the cached export. This is a singleton. In ESM, the module instance is also cached per URL. Class-based singletons add ceremony without benefit in most cases.
Anti-patterns:
- Global mutable state disguised as a singleton — prefer immutable config objects
- Singletons that make testing impossible — always allow injection or reset
- Singleton holding a connection without lifecycle management — always handle cleanup
- Thread-safe double-checked locking in TypeScript — Node.js is single-threaded, this is unnecessary
When to use dependency injection instead:
// Prefer this for services that need to be tested or swapped class OrderService { constructor(private readonly db: DatabasePool) {} } // Wire at app start const db = getPool(); const orderService = new OrderService(db);
Singleton vs. service locator: A singleton is discoverable from anywhere; a service locator is explicit injection. Singletons are fine for infrastructure (logger, config, pool) but anti-patterns for business logic.
Lazy initialization matters for startup cost: Do not initialize singletons at import time if they open connections — use lazy
getInstance() so tests don't need live infrastructure to import a module.
Source
refactoring.guru/design-patterns/singleton
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.