Registry
The Registry pattern centralizes registration and lookup of shared services by key, replacing scattered globals and enabling plugin discovery. Overuse as a service locator hides dependencies.
The Registry pattern provides a well-known object that other objects can use to find common services or instances by name or type. It answers a recurring question: how does code that does not own a dependency, and cannot easily receive it as a parameter, get hold of it? A registry offers a single, explicit place to register and retrieve shared objects instead of scattering global variables throughout the codebase.
How It Works
A registry holds a map from a key — a name, type, or token — to an instance or a factory. Code registers objects during startup (the composition phase) and later resolves them by key. The registry may store ready instances, lazily create them via factories, or scope them (per request, per thread). It is itself usually a single shared object, accessed statically or, better, injected.
Variants include the type-keyed registry (resolve by interface), the named registry (resolve by string), and the plugin registry, where modules self-register their capabilities at load time so a host can discover them without compile-time knowledge.
When to Use It
Use a registry for plugin and extension systems where components must be discovered dynamically, for mapping keys to handlers (command dispatch, serializer-by-type), and for providing a controlled access point to a bounded set of shared services. It is a clean way to implement open extensibility: new plugins register themselves and the core needs no changes.
Be wary of using a registry as a general dependency-acquisition mechanism for ordinary application code — that is the Service Locator anti-pattern, which hides dependencies behind a global lookup. Prefer constructor Dependency Injection there; reserve registries for genuine lookup and discovery scenarios.
Trade-offs
A registry centralizes access, replaces ad-hoc globals with one auditable structure, and enables dynamic, plugin-style extension and key-to-handler dispatch. It decouples the act of locating a service from the act of creating it.
The risks are the familiar ones of shared mutable state: it can become a hidden dependency that obscures what a class actually needs, complicating tests; lookups fail at runtime rather than compile time if a key is missing; and an unscoped registry can leak instances. Used as a blanket service locator it undermines the explicitness DI provides.
Related Patterns
Registry overlaps heavily with Multiton (a keyed set of single instances) and is the storage backbone of a Service Locator. A DI container is essentially a sophisticated, lifetime-aware registry. It often holds Factory objects and is the mechanism behind plugin architectures and Abstract Factory selection.
Example
A serialization library maintains a registry mapping content types to serializers: registry.register("application/json", new JsonSerializer()). When a request arrives, the framework resolves the serializer for the request's content type from the registry. Third-party packages add formats simply by registering their own serializer at load time, so the core dispatch code never changes as new formats appear.