Skip to content
SP StackPractices
intermediate By StackPractices

Multiton Pattern

Manage a map of named singleton instances, providing controlled access to a finite set of shared objects identified by keys.

Topics: design

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Multiton Pattern

Overview

The Multiton Pattern extends the Singleton concept to manage multiple named instances. Instead of a single global instance, a Multiton maintains a registry of instances keyed by name or identifier. Requesting the same key always returns the same instance, but different keys produce different instances.

This pattern is useful when you need a fixed set of related singletons — for example, database connection pools per tenant, logger instances per module, or theme configurations per client.

When to Use

Use the Multiton Pattern when:

  • You need a controlled set of singleton-like instances identified by keys
  • Resources are expensive and should be shared per category, not globally
  • You want to avoid creating instances for keys that are never used (lazy initialization)
  • The number of possible keys is finite and known

When to Avoid

  • Keys are dynamic or unbounded (use a generic cache or pool instead)
  • Instances are lightweight and cheap to create (direct instantiation is simpler)
  • You need lifecycle management per instance (use a factory with DI container)

Solution

Python

import threading

class DatabaseConnectionPool:
    _instances = {}
    _lock = threading.Lock()

    def __init__(self, tenant_id: str):
        self.tenant_id = tenant_id
        self.connections = []
        print(f"Created pool for tenant {tenant_id}")

    @classmethod
    def get_instance(cls, tenant_id: str):
        if tenant_id not in cls._instances:
            with cls._lock:
                if tenant_id not in cls._instances:
                    cls._instances[tenant_id] = cls(tenant_id)
        return cls._instances[tenant_id]

    def query(self, sql: str):
        return f"[{self.tenant_id}] Result for: {sql}"


# Usage
pool_a = DatabaseConnectionPool.get_instance("tenant-a")
pool_b = DatabaseConnectionPool.get_instance("tenant-b")
pool_a2 = DatabaseConnectionPool.get_instance("tenant-a")

print(pool_a is pool_a2)  # True — same instance
print(pool_a is pool_b)   # False — different instance

Java

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ThemeManager {
    private static final Map<String, ThemeManager> instances = new ConcurrentHashMap<>();
    private final String themeName;

    private ThemeManager(String themeName) {
        this.themeName = themeName;
        System.out.println("Created theme manager for " + themeName);
    }

    public static ThemeManager getInstance(String themeName) {
        return instances.computeIfAbsent(themeName, ThemeManager::new);
    }

    public String apply(String component) {
        return "[" + themeName + "] Styled " + component;
    }
}

// Usage
ThemeManager light = ThemeManager.getInstance("light");
ThemeManager dark = ThemeManager.getInstance("dark");
ThemeManager light2 = ThemeManager.getInstance("light");

System.out.println(light == light2); // true
System.out.println(light == dark);   // false

JavaScript

class Logger {
  static #instances = new Map();

  constructor(moduleName) {
    this.moduleName = moduleName;
    console.log(`Created logger for ${moduleName}`);
  }

  static getInstance(moduleName) {
    if (!Logger.#instances.has(moduleName)) {
      Logger.#instances.set(moduleName, new Logger(moduleName));
    }
    return Logger.#instances.get(moduleName);
  }

  log(message) {
    console.log(`[${this.moduleName}] ${message}`);
  }
}

// Usage
const dbLogger = Logger.getInstance('database');
const apiLogger = Logger.getInstance('api');
const dbLogger2 = Logger.getInstance('database');

console.log(dbLogger === dbLogger2); // true
console.log(dbLogger === apiLogger); // false

Explanation

The Multiton Pattern involves:

  • Registry: A map or dictionary storing instances keyed by identifier
  • Factory Method: getInstance(key) creates or returns the existing instance for that key
  • Private Constructor: Prevents direct instantiation outside the class
  • Thread Safety: Synchronization or atomic operations prevent duplicate creation under concurrency

Variants

VariantBehaviorUse Case
Lazy MultitonCreates on first accessLarge key spaces where most keys are unused
Eager MultitonPre-creates all instancesSmall, fixed set of keys (themes, environments)
Bounded MultitonEvicts oldest when fullMemory-sensitive caches with max capacity
Weak MultitonAllows GC when unreferencedTemporary per-request resources

Best Practices

  • Use thread-safe registries. Concurrent access to the instance map is the most common source of bugs.
  • Clean up unused instances. For dynamic keys, implement eviction or TTL to prevent unbounded growth.
  • Validate keys. Reject unknown or malformed keys instead of creating instances for them.
  • Document the key namespace. Multitons are hard to discover; document which keys are valid and what they represent.
  • Do not store mutable global state in multiton instances unless it is the intended behavior.

Common Mistakes

  • Unbounded key growth causes memory leaks when keys are generated dynamically (e.g., user IDs).
  • Race conditions during instance creation under load lead to duplicate instances for the same key.
  • Hardcoding keys in client code scatters configuration. Use constants or configuration-driven key selection.
  • Using Multiton as a cache — caches need eviction policies; multitons are for permanent singleton families.
  • Exposing the internal registry allows external code to modify or clear instances unpredictably.

Real-World Examples

Java Locale

NumberFormat.getCurrencyInstance(Locale.US) returns a shared formatter for US currency. Different locales return different singleton formatters.

Logging Frameworks

Log4j and SLF4J maintain named loggers per class or module. LoggerFactory.getLogger("com.myapp.db") always returns the same logger instance.

Connection Pools

Multi-tenant SaaS applications often maintain one database pool per tenant, accessed by PoolManager.get(tenantId).

Frequently Asked Questions

Q: What is the difference between Multiton and a regular Map? A: A Multiton controls instance creation (private constructor) and guarantees the same instance for the same key. A Map just stores externally created objects.

Q: Can I remove instances from a Multiton? A: Yes, but carefully. Provide a controlled evict(key) method for explicit cleanup rather than exposing the internal map.

Q: Is Multiton an anti-pattern? A: Like Singleton, it is not inherently bad but is easily abused. It is appropriate for finite, well-defined sets of shared resources.