Back to blog

Singleton Pattern: One Instance to Rule Them All

oopcreational-patternssingletondesign-patternstypescriptpythonjava
Singleton Pattern: One Instance to Rule Them All

Introduction

You've learned the SOLID principles—the guidelines for structuring classes and their relationships. Now it's time to learn design patterns: proven, reusable solutions to problems that come up again and again in software design.

We start with the simplest and most debated creational pattern: the Singleton.

The Singleton pattern ensures a class has exactly one instance and provides a global point of access to it. Sounds simple, right? Yet few patterns spark more debate. Used well, it solves real problems. Used carelessly, it becomes a source of tight coupling, hidden dependencies, and untestable code.

What You'll Learn

✅ Understand the problem the Singleton pattern solves
✅ Implement Singleton in TypeScript, Python, and Java
✅ Compare eager vs lazy vs thread-safe initialization
✅ Use the ES6 module pattern as a modern TypeScript alternative
✅ Understand why Singleton is called an anti-pattern by some
✅ Overcome testing challenges with dependency injection

Prerequisites


The Problem: Why Do We Need Singleton?

Some resources in your application should only exist once:

ResourceWhy Only One?
LoggerAll parts of the app should write to the same log
ConfigurationSettings loaded once, used everywhere
Database connection poolOne pool shared across all services
CacheOne shared cache to avoid duplicated data
Thread poolOne pool to manage concurrency

Without Singleton, you risk:

  1. Duplicate instances — Two loggers writing to different files, two caches with stale data
  2. Wasted resources — Each instance consumes memory and possibly file handles or connections
  3. Inconsistent state — Multiple config objects with different values

The Singleton pattern guarantees that a class has only one instance and provides a global access point to that instance.


Singleton Structure

Key components:

  1. Private constructor — Prevents anyone from calling new Singleton()
  2. Static instance field — Holds the single instance
  3. Static getInstance() method — Creates the instance on first call, returns the existing one on subsequent calls

Implementation: Lazy Initialization

The most common approach: create the instance only when first requested.

TypeScript

// TypeScript - Lazy Singleton
class Logger {
  private static instance: Logger;
  private logs: string[] = [];
 
  // Private constructor - no one can call `new Logger()`
  private constructor() {
    console.log("Logger initialized");
  }
 
  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
 
  public log(message: string): void {
    const entry = `[${new Date().toISOString()}] ${message}`;
    this.logs.push(entry);
    console.log(entry);
  }
 
  public getLogs(): string[] {
    return [...this.logs]; // Return copy to protect internal state
  }
}
 
// Usage
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
 
logger1.log("Application started");
logger2.log("User logged in");
 
console.log(logger1 === logger2);       // true — same instance
console.log(logger1.getLogs().length);   // 2 — both logs in one place

How it works:

  1. First call to getInstance()instance is undefined, so a new Logger is created
  2. Every subsequent call — instance already exists, returns it immediately
  3. The private constructor prevents new Logger() from outside the class

Python

# Python - Lazy Singleton using __new__
import threading
from datetime import datetime
 
 
class Logger:
    _instance = None
 
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._logs = []
            print("Logger initialized")
        return cls._instance
 
    def log(self, message: str) -> None:
        entry = f"[{datetime.now().isoformat()}] {message}"
        self._logs.append(entry)
        print(entry)
 
    def get_logs(self) -> list[str]:
        return self._logs.copy()
 
 
# Usage
logger1 = Logger()
logger2 = Logger()
 
logger1.log("Application started")
logger2.log("User logged in")
 
print(logger1 is logger2)        # True — same instance
print(len(logger1.get_logs()))   # 2 — both logs in one place

Python's __new__ vs __init__: __new__ controls instance creation (called before __init__). By overriding it, we intercept the creation step and return the existing instance instead of creating a new one.

Java

// Java - Lazy Singleton
public class Logger {
    private static Logger instance;
    private final List<String> logs = new ArrayList<>();
 
    // Private constructor
    private Logger() {
        System.out.println("Logger initialized");
    }
 
    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
 
    public void log(String message) {
        String entry = "[" + java.time.Instant.now() + "] " + message;
        logs.add(entry);
        System.out.println(entry);
    }
 
    public List<String> getLogs() {
        return List.copyOf(logs); // Immutable copy
    }
}
 
// Usage
Logger logger1 = Logger.getInstance();
Logger logger2 = Logger.getInstance();
 
logger1.log("Application started");
logger2.log("User logged in");
 
System.out.println(logger1 == logger2);       // true — same instance
System.out.println(logger1.getLogs().size());  // 2

Implementation: Eager Initialization

Create the instance when the class is loaded, not when first requested.

TypeScript

// TypeScript - Eager Singleton
class AppConfig {
  // Instance created immediately when class is loaded
  private static readonly instance: AppConfig = new AppConfig();
 
  private settings: Map<string, string> = new Map();
 
  private constructor() {
    // Load default settings
    this.settings.set("env", "production");
    this.settings.set("logLevel", "info");
    console.log("AppConfig loaded");
  }
 
  public static getInstance(): AppConfig {
    return AppConfig.instance;
  }
 
  public get(key: string): string | undefined {
    return this.settings.get(key);
  }
 
  public set(key: string, value: string): void {
    this.settings.set(key, value);
  }
}

Python

# Python - Eager Singleton using module-level instance
class _AppConfig:
    def __init__(self):
        self._settings: dict[str, str] = {
            "env": "production",
            "log_level": "info",
        }
        print("AppConfig loaded")
 
    def get(self, key: str) -> str | None:
        return self._settings.get(key)
 
    def set(self, key: str, value: str) -> None:
        self._settings[key] = value
 
 
# Module-level instance — created when module is first imported
config = _AppConfig()
 
 
# Usage (from another module):
# from config import config
# config.get("env")  # "production"

Java

// Java - Eager Singleton
public class AppConfig {
    // Instance created when class is loaded by the JVM
    private static final AppConfig INSTANCE = new AppConfig();
 
    private final Map<String, String> settings = new HashMap<>();
 
    private AppConfig() {
        settings.put("env", "production");
        settings.put("logLevel", "info");
        System.out.println("AppConfig loaded");
    }
 
    public static AppConfig getInstance() {
        return INSTANCE;
    }
 
    public String get(String key) {
        return settings.get(key);
    }
 
    public void set(String key, String value) {
        settings.put(key, value);
    }
}

Lazy vs Eager: When to Use Each

AspectLazy InitializationEager Initialization
Instance createdOn first getInstance() callWhen class is loaded
Startup costDeferred (faster startup)Upfront (slower startup)
Thread safetyRequires extra handlingInherently thread-safe
Resource usageOnly if neededAlways allocated
Best forExpensive or rarely-used resourcesLightweight, always-needed resources

Thread Safety (Java Focus)

The lazy Singleton above has a problem in multithreaded environments:

// What happens with two threads calling getInstance() at the same time?
//
// Thread A: checks instance == null → true
// Thread B: checks instance == null → true (before A creates it!)
// Thread A: creates instance #1
// Thread B: creates instance #2  ← Two instances exist!

Solution 1: Synchronized Method

// Java - Thread-safe with synchronized
public class Logger {
    private static Logger instance;
 
    private Logger() {}
 
    // synchronized: only one thread can enter at a time
    public static synchronized Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
}

Problem: Every call to getInstance() acquires a lock, even after the instance is created. This adds unnecessary overhead.

Solution 2: Double-Checked Locking

// Java - Double-checked locking
public class Logger {
    // volatile ensures all threads see the latest value
    private static volatile Logger instance;
 
    private Logger() {}
 
    public static Logger getInstance() {
        if (instance == null) {                // First check (no lock)
            synchronized (Logger.class) {      // Lock only when needed
                if (instance == null) {        // Second check (with lock)
                    instance = new Logger();
                }
            }
        }
        return instance;
    }
}

Why two checks? The first check avoids locking on every call. The second check prevents the race condition described above. The volatile keyword ensures the instance is fully constructed before other threads see it.

// Java - Bill Pugh Singleton (uses inner static class)
public class Logger {
    private final List<String> logs = new ArrayList<>();
 
    private Logger() {
        System.out.println("Logger initialized");
    }
 
    // Inner class is not loaded until getInstance() is called
    private static class Holder {
        private static final Logger INSTANCE = new Logger();
    }
 
    public static Logger getInstance() {
        return Holder.INSTANCE;
    }
 
    public void log(String message) {
        String entry = "[" + java.time.Instant.now() + "] " + message;
        logs.add(entry);
        System.out.println(entry);
    }
}

Why this is the best Java approach:

  • Lazy — The inner class Holder isn't loaded until getInstance() is called
  • Thread-safe — The JVM guarantees that class loading is thread-safe
  • No synchronization overhead — No synchronized or volatile needed
  • Simple — Clean, easy to understand

Thread Safety Comparison

ApproachThread-Safe?PerformanceComplexity
Lazy (no sync)NoFastestSimple
Synchronized methodYesSlowest (lock on every call)Simple
Double-checked lockingYesFast (lock only on first call)Complex
Bill Pugh (inner class)YesFastSimple
Eager initializationYesFast (allocated on class load)Simplest

The ES6 Module Pattern (TypeScript/JavaScript)

In modern JavaScript/TypeScript, modules are Singletons by default. When you import a module, Node.js caches it — subsequent imports return the same cached instance.

// logger.ts — ES6 module Singleton (no class needed!)
 
const logs: string[] = [];
 
export function log(message: string): void {
  const entry = `[${new Date().toISOString()}] ${message}`;
  logs.push(entry);
  console.log(entry);
}
 
export function getLogs(): string[] {
  return [...logs];
}
// app.ts
import { log, getLogs } from "./logger";
 
log("Application started");
console.log(getLogs().length); // 1
// userService.ts
import { log } from "./logger";
 
log("User logged in");
// This writes to the SAME logs array as app.ts

Why this works: Node.js evaluates a module once and caches the result. Every file that imports logger.ts gets the same logs array and the same functions. No class, no getInstance(), no private constructor needed.

Class-Based vs Module-Based Singleton

FeatureClass SingletonModule Singleton
BoilerplatePrivate constructor, static methodJust export functions/objects
Instantiation controlExplicit getInstance()Automatic (module caching)
TypeScript/JavaScriptWorks but verboseIdiomatic and recommended
TestingHard to reset stateEasier to mock with jest.mock()
Other languagesUniversal patternJS/TS specific

Best practice for TypeScript/JavaScript: Prefer the module pattern over the class-based Singleton. Reserve the class-based approach for when you need to implement an interface or work with frameworks that expect class instances.


Real-World Example: Database Connection Pool

Let's build a practical Singleton — a database connection pool that should be shared across your entire application.

TypeScript

// TypeScript - Database Connection Pool Singleton
interface ConnectionConfig {
  host: string;
  port: number;
  database: string;
  maxConnections: number;
}
 
class DatabasePool {
  private static instance: DatabasePool;
  private connections: string[] = [];
  private config: ConnectionConfig;
 
  private constructor(config: ConnectionConfig) {
    this.config = config;
    this.initializePool();
    console.log(
      `Database pool created: ${config.host}:${config.port}/${config.database}`
    );
  }
 
  public static initialize(config: ConnectionConfig): DatabasePool {
    if (!DatabasePool.instance) {
      DatabasePool.instance = new DatabasePool(config);
    }
    return DatabasePool.instance;
  }
 
  public static getInstance(): DatabasePool {
    if (!DatabasePool.instance) {
      throw new Error(
        "DatabasePool not initialized. Call initialize() first."
      );
    }
    return DatabasePool.instance;
  }
 
  private initializePool(): void {
    for (let i = 0; i < this.config.maxConnections; i++) {
      this.connections.push(`conn-${i}`);
    }
  }
 
  public getConnection(): string {
    const conn = this.connections.pop();
    if (!conn) {
      throw new Error("No available connections in pool");
    }
    console.log(`Checked out connection: ${conn}`);
    return conn;
  }
 
  public releaseConnection(conn: string): void {
    this.connections.push(conn);
    console.log(`Released connection: ${conn}`);
  }
 
  public getAvailableCount(): number {
    return this.connections.length;
  }
}
 
// Application startup
DatabasePool.initialize({
  host: "localhost",
  port: 5432,
  database: "myapp",
  maxConnections: 10,
});
 
// In any service — gets the same pool
const pool = DatabasePool.getInstance();
const conn = pool.getConnection();
// ... use connection ...
pool.releaseConnection(conn);

Python

# Python - Database Connection Pool Singleton
from dataclasses import dataclass
from collections import deque
 
 
@dataclass
class ConnectionConfig:
    host: str
    port: int
    database: str
    max_connections: int
 
 
class DatabasePool:
    _instance = None
 
    def __new__(cls, config: ConnectionConfig | None = None):
        if cls._instance is None:
            if config is None:
                raise RuntimeError(
                    "DatabasePool not initialized. Pass config on first call."
                )
            cls._instance = super().__new__(cls)
            cls._instance._config = config
            cls._instance._connections = deque(
                f"conn-{i}" for i in range(config.max_connections)
            )
            print(
                f"Database pool created: "
                f"{config.host}:{config.port}/{config.database}"
            )
        return cls._instance
 
    def get_connection(self) -> str:
        if not self._connections:
            raise RuntimeError("No available connections in pool")
        conn = self._connections.pop()
        print(f"Checked out connection: {conn}")
        return conn
 
    def release_connection(self, conn: str) -> None:
        self._connections.append(conn)
        print(f"Released connection: {conn}")
 
    @property
    def available_count(self) -> int:
        return len(self._connections)
 
 
# Application startup
pool = DatabasePool(ConnectionConfig("localhost", 5432, "myapp", 10))
 
# In any module — same instance
pool = DatabasePool()
conn = pool.get_connection()
# ... use connection ...
pool.release_connection(conn)

Java

// Java - Database Connection Pool Singleton (Bill Pugh pattern)
public class DatabasePool {
    private final Queue<String> connections = new LinkedList<>();
    private final ConnectionConfig config;
 
    private DatabasePool() {
        this.config = ConnectionConfig.load(); // Load from environment/file
        for (int i = 0; i < config.maxConnections(); i++) {
            connections.add("conn-" + i);
        }
        System.out.printf(
            "Database pool created: %s:%d/%s%n",
            config.host(), config.port(), config.database()
        );
    }
 
    private static class Holder {
        private static final DatabasePool INSTANCE = new DatabasePool();
    }
 
    public static DatabasePool getInstance() {
        return Holder.INSTANCE;
    }
 
    public synchronized String getConnection() {
        if (connections.isEmpty()) {
            throw new RuntimeException("No available connections in pool");
        }
        String conn = connections.poll();
        System.out.println("Checked out connection: " + conn);
        return conn;
    }
 
    public synchronized void releaseConnection(String conn) {
        connections.add(conn);
        System.out.println("Released connection: " + conn);
    }
 
    public synchronized int getAvailableCount() {
        return connections.size();
    }
}
 
// Usage in any service
DatabasePool pool = DatabasePool.getInstance();
String conn = pool.getConnection();
// ... use connection ...
pool.releaseConnection(conn);

The Anti-Pattern Debate

The Singleton is the most criticized of the Gang of Four patterns. Here's why:

Arguments Against Singleton

1. Hidden Dependencies

// Bad — Where does UserService get its database?
class UserService {
  createUser(name: string): void {
    // Hidden dependency — impossible to know without reading the code
    const db = DatabasePool.getInstance();
    db.query(`INSERT INTO users (name) VALUES ('${name}')`);
  }
}
 
// Good — Dependencies are explicit
class UserService {
  constructor(private db: DatabasePool) {}
 
  createUser(name: string): void {
    this.db.query(`INSERT INTO users (name) VALUES ('${name}')`);
  }
}

2. Global Mutable State

Singletons are essentially global variables wrapped in a class. Any code anywhere can change the state:

// Module A changes config
AppConfig.getInstance().set("timeout", "5000");
 
// Module B reads config later — affected by Module A
const timeout = AppConfig.getInstance().get("timeout");
// Is it "5000"? The default? Who knows without tracing every call?

3. Violates Single Responsibility Principle

A Singleton class manages two things: its actual business logic and its own lifecycle (creation and access). This violates the SRP from the SOLID principles you just learned.

4. Tight Coupling

Every class that calls Singleton.getInstance() is coupled to that specific concrete class. You can't substitute an alternative implementation without modifying the calling code.

Arguments For Singleton

Use CaseWhy Singleton Works
Framework infrastructureLogging, metrics, config — genuinely app-wide concerns
Resource managementConnection pools, thread pools — one shared pool is correct
CachingOne cache avoids data duplication
Hardware accessPrinter spooler, device driver — one physical resource

The Verdict

Don't avoid Singleton entirely — avoid using it as a default. If you're reaching for Singleton because it's convenient, stop. If you're using it because there genuinely must be only one instance of something, proceed with caution and consider dependency injection.


Testing Challenges and Solutions

The biggest practical problem with Singletons is testing. Since the instance persists across tests, one test can affect another.

The Problem

// Test 1: Adds entries to the logger
test("should log messages", () => {
  const logger = Logger.getInstance();
  logger.log("test message");
  expect(logger.getLogs()).toHaveLength(1); // ✅ Passes
});
 
// Test 2: Expects a fresh logger — but gets Test 1's state!
test("should start with empty logs", () => {
  const logger = Logger.getInstance();
  expect(logger.getLogs()).toHaveLength(0); // ❌ FAILS — has 1 entry from Test 1
});

Solution 1: Add a Reset Method (For Testing Only)

class Logger {
  private static instance: Logger;
  private logs: string[] = [];
 
  private constructor() {}
 
  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
 
  public log(message: string): void {
    this.logs.push(`[${new Date().toISOString()}] ${message}`);
  }
 
  public getLogs(): string[] {
    return [...this.logs];
  }
 
  // Only for testing — allows resetting state between tests
  public static resetInstance(): void {
    Logger.instance = undefined as unknown as Logger;
  }
}
 
// In tests
afterEach(() => {
  Logger.resetInstance();
});

Instead of accessing the Singleton directly, inject it as a dependency:

// Define an interface
interface ILogger {
  log(message: string): void;
  getLogs(): string[];
}
 
// Singleton implements the interface
class Logger implements ILogger {
  private static instance: Logger;
  private logs: string[] = [];
 
  private constructor() {}
 
  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
 
  public log(message: string): void {
    this.logs.push(`[${new Date().toISOString()}] ${message}`);
  }
 
  public getLogs(): string[] {
    return [...this.logs];
  }
}
 
// Service depends on the interface, not the Singleton
class OrderService {
  constructor(private logger: ILogger) {}
 
  placeOrder(item: string): void {
    this.logger.log(`Order placed: ${item}`);
  }
}
 
// Production — inject the Singleton
const orderService = new OrderService(Logger.getInstance());
 
// Testing — inject a mock
class MockLogger implements ILogger {
  public messages: string[] = [];
 
  log(message: string): void {
    this.messages.push(message);
  }
 
  getLogs(): string[] {
    return this.messages;
  }
}
 
test("should log when order is placed", () => {
  const mockLogger = new MockLogger();
  const service = new OrderService(mockLogger);
 
  service.placeOrder("Laptop");
 
  expect(mockLogger.messages).toHaveLength(1);
  expect(mockLogger.messages[0]).toContain("Laptop");
});
# Python - Dependency Injection with Singleton
from abc import ABC, abstractmethod
 
 
class ILogger(ABC):
    @abstractmethod
    def log(self, message: str) -> None: ...
 
    @abstractmethod
    def get_logs(self) -> list[str]: ...
 
 
class Logger(ILogger):
    _instance = None
 
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._logs = []
        return cls._instance
 
    def log(self, message: str) -> None:
        self._logs.append(message)
 
    def get_logs(self) -> list[str]:
        return self._logs.copy()
 
 
class OrderService:
    def __init__(self, logger: ILogger):
        self._logger = logger
 
    def place_order(self, item: str) -> None:
        self._logger.log(f"Order placed: {item}")
 
 
# Production
service = OrderService(Logger())
 
# Testing
class MockLogger(ILogger):
    def __init__(self):
        self.messages: list[str] = []
 
    def log(self, message: str) -> None:
        self.messages.append(message)
 
    def get_logs(self) -> list[str]:
        return self.messages
 
 
def test_place_order():
    mock = MockLogger()
    service = OrderService(mock)
    service.place_order("Laptop")
    assert len(mock.messages) == 1
    assert "Laptop" in mock.messages[0]
// Java - Dependency Injection with Singleton
public interface ILogger {
    void log(String message);
    List<String> getLogs();
}
 
public class Logger implements ILogger {
    private static class Holder {
        private static final Logger INSTANCE = new Logger();
    }
 
    private final List<String> logs = new ArrayList<>();
    private Logger() {}
 
    public static Logger getInstance() {
        return Holder.INSTANCE;
    }
 
    @Override
    public void log(String message) {
        logs.add("[" + java.time.Instant.now() + "] " + message);
    }
 
    @Override
    public List<String> getLogs() {
        return List.copyOf(logs);
    }
}
 
public class OrderService {
    private final ILogger logger;
 
    // Inject the dependency — not tied to Logger.getInstance()
    public OrderService(ILogger logger) {
        this.logger = logger;
    }
 
    public void placeOrder(String item) {
        logger.log("Order placed: " + item);
    }
}
 
// Production
OrderService service = new OrderService(Logger.getInstance());
 
// Testing
ILogger mockLogger = mock(ILogger.class);
OrderService testService = new OrderService(mockLogger);
testService.placeOrder("Laptop");
verify(mockLogger).log(contains("Laptop"));

This approach gives you the best of both worlds: one instance in production (the Singleton), but full testability (through the interface).


When to Use Singleton

Good Use Cases

Use CaseExampleWhy Singleton Fits
LoggingApplication-wide loggerAll modules should write to one destination
ConfigurationApp settings from env/fileLoad once, read many times
Connection poolsDatabase, HTTP client poolsOne shared pool is the correct design
CachesIn-memory cache layerOne cache prevents data inconsistency
Feature flagsRuntime feature togglesOne source of truth for flags

When NOT to Use Singleton

ScenarioWhy NotAlternative
"I need this everywhere"Convenience ≠ good designPass via constructor (DI)
Stateless servicesNo shared state to protectCreate instances freely
Per-request scopeWeb requests need isolationRequest-scoped instances
Unit testingGlobal state breaks test isolationInterface + DI
Multiple configurationsDifferent DB pools for read/writeFactory pattern instead

Decision Flowchart


Singleton vs Other Patterns

PatternPurposeInstances
SingletonEnsure one instance, global accessExactly 1
FactoryCreate objects without specifying concrete classMany (one per call)
Dependency InjectionProvide dependencies externallyManaged by container
Static classStateless utility functionsNo instances (all static)

Singleton vs Static Class:

A static class (utility class with only static methods) has no state and no instance. Use it for pure utility functions like Math.max() or StringUtils.capitalize(). Use Singleton when you need state (like a log buffer or connection pool) that is shared across the application.


Summary and Key Takeaways

The Singleton Pattern

✅ Ensures a class has exactly one instance with global access
✅ Uses a private constructor and a static getInstance() method
✅ In TypeScript/JS, prefer the ES6 module pattern over class-based Singleton
✅ In Java, prefer the Bill Pugh inner class pattern for thread safety
✅ Always inject Singletons through interfaces for testability
✅ Use Singleton for infrastructure concerns (logging, config, connection pools)

Common Pitfalls to Avoid

  • Don't use Singleton just for convenience — If you use it because "I need this everywhere", use dependency injection instead
  • Don't hide dependencies — Make Singleton usage explicit through constructor injection
  • Don't forget thread safety — In multithreaded environments (Java), use Bill Pugh or eager initialization
  • Don't store request-specific state — Singletons live for the application's lifetime, not per-request

Quick Reference

LanguageRecommended Approach
TypeScript/JSES6 module pattern (export functions/objects)
PythonModule-level instance or __new__ override
JavaBill Pugh inner static class

Practice Exercises

  1. Build a Configuration Manager: Create a Singleton ConfigManager that reads settings from a dictionary/map (simulating a config file). It should support get(key), set(key, value), and getAll(). Then write a test that uses a mock IConfigManager interface instead.

  2. Thread-Safe Counter: Implement a Singleton RequestCounter that tracks the total number of HTTP requests. In Java, make it thread-safe using the Bill Pugh pattern with synchronized increment/read methods. In Python, use threading.Lock.

  3. Refactor Away a Singleton: Take this code and refactor it to use dependency injection instead of getInstance():

    class NotificationService {
      notify(userId: string, message: string): void {
        Logger.getInstance().log(`Notifying ${userId}: ${message}`);
        EmailService.getInstance().send(userId, message);
        MetricsService.getInstance().increment("notifications_sent");
      }
    }

What's Next?

The Singleton ensures one instance. But what about when you need to create many objects without knowing their exact types? That's where the Factory patterns come in:

  • Factory Method — Define an interface for creating objects, let subclasses decide which class to instantiate
  • Abstract Factory — Create families of related objects without specifying concrete classes
  • When to use Factory vs Abstract Factory vs Builder

Continue your OOP journey: Factory and Abstract Factory Patterns


Additional Resources

Previous Posts in This Series

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.