Multiton Pattern
Manage a map of named singleton instances, providing controlled access to a finite set of shared objects identified by keys.
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
- For alternatives, see Singleton Pattern.
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
| Variant | Behavior | Use Case |
|---|---|---|
| Lazy Multiton | Creates on first access | Large key spaces where most keys are unused |
| Eager Multiton | Pre-creates all instances | Small, fixed set of keys (themes, environments) |
| Bounded Multiton | Evicts oldest when full | Memory-sensitive caches with max capacity |
| Weak Multiton | Allows GC when unreferenced | Temporary 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. Related Resources
Singleton Pattern
Ensure a class has only one instance and provide global access to it. A creational design pattern for controlled object creation.
PatternFactory Pattern
Create objects without specifying the exact class to instantiate. A creational design pattern for flexible object creation.
PatternObject Pool Pattern
Reuse expensive objects instead of creating and destroying them repeatedly. A creational pattern for managing scarce resources efficiently.
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.