Home Design Patterns Multiton
Multiton
Modern PracticeOne instance per key, instead of one instance overall.
Purpose
A Multiton is a keyed registry of singletons. A static map holds one instance per key, a static accessor returns the existing instance or creates it, and the constructor is private so there is no other way in. Singleton is the special case where the key space has one member.
The pull is real enough. Some things genuinely are one-per-something, and the something is not the process: one command channel per arm, one lock per record id, one logger per category, one configuration per tenant. Singleton answers "one per process" and leaves everything else to you, so generalizing it to a key looks like the obvious next step. It is also where the trouble starts, because everything that made Singleton a bad idea is still here, and it now has a growth term.
Design
The mechanics are a dictionary, a lock and a private constructor, and there is nothing clever about any of them. Two details are worth stating because they are where implementations go wrong. The lock is not optional: without it two threads racing on a new key each construct their own instance, both write to the map, and one of them wins. That defeats the entire purpose, and it fails intermittently under load rather than in a test. And there is no counterpart to the creation path. Nothing removes entries, because a static map has no owner to decide when a value's life is over.
Example
Autobot Robotics's controller opened a link to an arm whenever it needed one. That was fine while the scheduler was the only thing talking to hardware. When the diagnostics tool shipped, both subsystems opened links to the same arm, and the arm firmware accepts exactly one command channel: it took commands from both, interleaved them, and faulted mid-move with a following error that looked like a mechanical problem. Two weeks were spent on the mechanics before anyone looked at the channel.
The fix was ArmLink.forSerial(serial): a static map from arm
serial number to link, a private constructor, and a lock. It worked immediately
and it was three dozen lines. For about a year it was the good decision.
What ended it was a plant survey tool that walks a serial range to find which
arms are present and reachable. Every serial it probed put a permanent entry in
the map, each holding a socket, and the controller ran out of file descriptors
after a few days of uptime. There was no place to put the fix, because there was
nothing that owned the map and therefore nothing that could decide an entry was
finished with. The replacement was an ordinary ArmLinkRegistry
object, constructed at startup, handed to the scheduler and the diagnostics tool,
with an explicit release. It solved the original interleaving problem
just as well, and it also had somewhere to put the eviction. The only thing lost
was the ability to reach a link from a static method anywhere in the program, and
losing that was the point.
class ArmLink
{
private static Dictionary<string, ArmLink> links
= new Dictionary<string, ArmLink>();
private static object gate = new object();
private string serial;
private ArmLink(string s)
{
serial = s;
// opens a socket to the controller
}
public static ArmLink forSerial(string s)
{
lock (gate)
{
if (!links.ContainsKey(s))
{
links[s] = new ArmLink(s); // and nothing ever removes it
}
return links[s];
}
}
public void send(Command c) { /* some code */ }
}
class Scheduler
{
public void move(string serial)
{
// No constructor argument, no field, no wiring. That
// convenience is the whole appeal and the whole problem.
ArmLink.forSerial(serial).send(/* some code */);
}
}
lock serializes every
caller of every key through one gate to protect a check that only matters on
first use.Seen in the Wild
Logging is where the pattern earns its keep. Python's
logging.getLogger(name) is documented as returning the same logger
object for the same name, and SLF4J and Log4j do the same through
LoggerFactory.getLogger, keeping a map of loggers by category for the
life of the process. .NET's System.Text.Encoding.GetEncoding returns
cached encoding instances rather than fresh ones, and Java's
java.util.TimeZone.getTimeZone and Charset.forName cache
in the same way. It is worth noticing what these have in common: the key space is
bounded and derived from the program, not from data. There is one logger per class
and a few hundred charsets, and none of them own an operating system resource.
That is the case where a multiton is defensible, and it is not the case most
people use it for.
Comparisons
- Singleton is a Multiton with one key. Every criticism made of Singleton applies here unchanged, which is most of what there is to say about this pattern.
- Flyweight is the near-identical implementation with the opposite purpose, and the distinction is worth being precise about. Flyweight shares immutable intrinsic state to save memory, and does not care whether you get the same instance twice. Multiton exists specifically so that you do get the same instance, because the instance has identity and usually owns something. Sharing an immutable glyph is safe; sharing a mutable connection is a coordination problem you have just made global.
- Object Pool looks similar and behaves quite differently: a pool lends an object out and expects it back, which means it has a lifetime model and can bound its size. A multiton has neither.
- Factory Method is what
forSerialis, structurally. Making it static is what turns a reasonable factory into a global. - Dependency Injection is the alternative. An ordinary object holding the same map, constructed once and passed in, gives you the uniqueness guarantee without the global.
Why It Exists
My take
The pressure is genuine and worth granting before criticizing the answer. Uniqueness per key is a real requirement. If two objects both hold a command channel to one arm, the arm faults, and no amount of careful calling convention fixes it, because the second caller is in a different subsystem written by someone else. Something has to guarantee that asking twice yields the same object. That much is not in dispute.
What is in dispute is why the guarantee has to be static. Building one-per-key is not the hard part; it is a dictionary and a lock, and anybody can write it in ten minutes. The reason people reach for a Multiton is that they want it reachable from anywhere without passing it through a constructor, and that is precisely the Singleton bargain: trade explicit wiring for a global, and pay for it in every test you write afterwards. The keying is incidental. It looks like a new pattern and it is an old one with a map in it.
The extra thing Multiton contributes on its own is unbounded growth, and I would put it near the top of the list rather than treat it as an implementation detail. Singleton allocates one object. A Multiton allocates one per distinct key it has ever been asked for, and the key is very often data: a serial number, a tenant identifier, a customer id, a file path, sometimes something a user typed. Nothing evicts, because eviction is a lifetime decision and there is no owner to make it. In a service that restarts nightly you might never notice. In a controller that is expected to run for months, a map keyed on operator input and holding a file descriptor per entry is not a leak, it is a denial of service with extra steps, and I have watched exactly that happen.
Criticisms
Start with everything inherited from Singleton, because it is all still here.
The state is global and mutable. The dependency is hidden: a class that calls
ArmLink.forSerial does not declare that it talks to hardware, so you
cannot tell what a class needs by reading its constructor, and you cannot
substitute a fake without a static reset hook that exists only for tests.
Initialization order becomes something you have to reason about. And the claim
"there is one of these per key in this process" is a statement about deployment,
not about the domain, so the class is asserting something it has no way to
know.
Then add the growth. The map only ever gets larger, and a weak-reference map is not the fix people hope it is: it converts a leak into a much subtler bug, because now an entry can vanish between two uses and a caller that expected a live connection gets a new one built underneath it. If the value owns a socket, a file handle or a lock, weak references also give you no defined point at which it is released. Eviction needs an owner and a policy, and this pattern is defined by having neither.
Concurrency is worse than it first appears in both directions. The obvious implementation locks a single gate, so every lookup of every key serializes through it to protect a check that only matters the first time. Move to a concurrent map and the usual API will happily construct two instances and throw one away, which is fine for a cached value and wrong for anything whose constructor opens a connection. Finally, tests share the map, so test order starts to matter: whatever the first test left under a key is what the fourth test receives, and you get a suite that passes cleanly and fails when someone adds a case in the middle. If you want one per key, own the map, give it a lifetime, and pass it to the code that needs it. The dictionary was never the problem.