intermediate By Mathias Paulenko

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.

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 live 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

What Works

  • Use thread-safe registries. Concurrent access to the instance map is the most common source of bugs.
  • Clean up unused instances. For live 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 live (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).

Troubleshooting

  • Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
  • Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
  • Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
  • Tests break when the design changes: favor stable contracts over internal structure.
  • Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.

Further Reading

  • Official documentation: check the current reference for the framework or tool used.
  • Related guides: explore the pattern and design-pattern guides for deeper coverage.
  • Complementary patterns: review design patterns applicable to your technology stack.
  • Public postmortems: study real incidents from teams that faced similar production issues.

Production Notes

  • Deploy gradually using canary or blue-green to catch regressions early.
  • Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
  • Document the rollback in the runbook; test the procedure in staging at least once per quarter.
  • Review structured logs with correlation IDs to trace requests end-to-end during incidents.

Key Takeaways

  • Apply multiton pattern when you need a practical solution for your use case.
  • Monitor performance after implementation; measure latency, errors, and resource usage before and after.
  • Check the Troubleshooting section for common failures; most have documented root causes with fixes.
  • Keep dependencies updated and run tests in CI to prevent production regressions.

Advanced Topics

Scenario: Multiton for Multi-tenant Connections

// Multiton: singleton with key, one instance per key
class TenantDatabase {
  private static instances = new Map<string, TenantDatabase>();
  private pool: Pool;

  private constructor(private tenantId: string, config: DBConfig) {
    this.pool = createPool({
      host: config.host,
      port: config.port,
      database: `tenant_${tenantId}`,
      max: 10,
    });
  }

  static getInstance(tenantId: string, config?: DBConfig): TenantDatabase {
    if (!this.instances.has(tenantId)) {
      if (!config) throw new Error(`Config required for new tenant: ${tenantId}`);
      this.instances.set(tenantId, new TenantDatabase(tenantId, config));
    }
    return this.instances.get(tenantId)!;
  }

  async query(sql: string, params: unknown[]) {
    return this.pool.query(sql, params);
  }

  static async closeAll(): Promise<void> {
    for (const instance of this.instances.values()) {
      await instance.pool.end();
    }
    this.instances.clear();
  }

  static getActiveTenants(): string[] {
    return [...this.instances.keys()];
  }
}

// Usage: one DB connection per tenant
const tenantA = TenantDatabase.getInstance("tenant-a", { host: "localhost", port: 5432 });
const tenantB = TenantDatabase.getInstance("tenant-b", { host: "localhost", port: 5432 });
const tenantA2 = TenantDatabase.getInstance("tenant-a"); // same instance

console.log(tenantA === tenantA2); // true
console.log(tenantA === tenantB); // false
console.log(TenantDatabase.getActiveTenants()); // ["tenant-a", "tenant-b"]

// Comparison: Singleton vs Multiton
  | Pattern | Instances | Key | Use case |
  |---------|-----------|-----|----------|
  | Singleton | 1 global | N/A | Logger, config |
  | Multiton | N per key | string/enum | Multi-tenant, multi-DB |
  | Factory | N unlimited | N/A | Create varied objects |
  | Object Pool | N limited | N/A | Reuse expensive objects |

Lessons:

  • Multiton is Singleton with key: one instance per key
  • Ideal for multi-tenant: one DB connection per tenant
  • closeAll() to clean up at shutdown
  • Map to store instances: O(1) lookup
  • In tests, always reset with closeAll() between suites

### How do I prevent memory leaks with Multiton?

Call closeAll() or removeInstance(key) when a tenant is no longer active. Implement a TTL or LRU eviction: if there are more than N active tenants, close the least recently used. In K8s, pods get recycled, but in long-running processes, inactive tenants can accumulate. Monitor getActiveTenants() and alert if it grows without bound.


End of document. Review and update quarterly.

## Common Production Pitfalls

- Applying the pattern where no abstraction is needed, adding accidental complexity.
- Letting the pattern leak into unrelated modules and blur ownership boundaries.
- Over-engineering the first implementation instead of starting simple and measuring pain.
- Skipping contract tests, so refactors silently break consumers.
- Ignoring failure modes that the pattern does not cover.
- Using the pattern as a default instead of choosing the right tool for the current scale.
- Forgetting to document when to stop using the pattern and what replaces it.
- Missing observability around the pattern's performance and error propagation.

Frequently Asked Questions

What is the difference between Multiton and a regular Map?
A Multiton controls instance creation (private constructor) and guarantees the same instance for the same key. A Map just stores externally created objects.
Can I remove instances from a Multiton?
Yes, but carefully. Provide a controlled evict(key) method for explicit cleanup rather than exposing the internal map.
Is Multiton an anti-pattern?
Like Singleton, it is not inherently bad but is easily abused. It is appropriate for finite, well-defined sets of shared resources.