intermediate By Mathias Paulenko

Registry Pattern

Centralize access to shared services and objects via a lookup table. A structural pattern that decouples consumers from concrete implementations.

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 Registry Pattern provides a centralized lookup mechanism for shared services, configurations, or objects. Instead of passing dependencies through long chains of constructors, components request what they need from a registry. This decouples consumers from the concrete implementations they use.

While similar to a Service Locator, the Registry Pattern is broader: it can store anything from database connections to feature flags, not just services. It is a pragmatic alternative to full dependency injection when DI frameworks are unavailable.

When to Use

Use the Registry Pattern when:

  • Multiple components need access to the same shared resource
  • Constructor injection would create excessively long parameter lists
  • You need runtime lookup based on configuration or context
  • A lightweight alternative to a full DI container is preferred

When to Avoid

  • The codebase has a proper DI framework (Spring, Angular, Dagger)
  • Registry lookups hide dependencies and make testing harder
  • The registry becomes a global state dumping ground
  • You need compile-time type safety for all dependencies

Solution

Python

from typing import Dict, Any, Callable, TypeVar

T = TypeVar('T')

class Registry:
    _store: Dict[str, Any] = {}
    _factories: Dict[str, Callable[[], Any]] = {}

    @classmethod
    def register(cls, name: str, instance: Any):
        cls._store[name] = instance

    @classmethod
    def register_factory(cls, name: str, factory: Callable[[], Any]):
        cls._factories[name] = factory

    @classmethod
    def get(cls, name: str) -> Any:
        if name in cls._store:
            return cls._store[name]
        if name in cls._factories:
            instance = cls._factories[name]()
            cls._store[name] = instance
            return instance
        raise KeyError(f"No registration found for: {name}")

    @classmethod
    def has(cls, name: str) -> bool:
        return name in cls._store or name in cls._factories


# Usage
class DatabaseConnection:
    def query(self, sql: str):
        return f"Result: {sql}"

class CacheClient:
    def get(self, key: str):
        return f"cached-{key}"

Registry.register("db", DatabaseConnection())
Registry.register_factory("cache", lambda: CacheClient())

db = Registry.get("db")
cache = Registry.get("cache")
print(db.query("SELECT 1"))
print(cache.get("users"))

Java

import java.util.*;
import java.util.function.Supplier;

public class Registry {
    private static final Map<String, Object> instances = new HashMap<>();
    private static final Map<String, Supplier<?>> factories = new HashMap<>();

    public static void register(String name, Object instance) {
        instances.put(name, instance);
    }

    public static void registerFactory(String name, Supplier<?> factory) {
        factories.put(name, factory);
    }

    @SuppressWarnings("unchecked")
    public static <T> T get(String name) {
        if (instances.containsKey(name)) {
            return (T) instances.get(name);
        }
        if (factories.containsKey(name)) {
            T instance = (T) factories.get(name).get();
            instances.put(name, instance);
            return instance;
        }
        throw new IllegalArgumentException("No registration for: " + name);
    }

    public static boolean has(String name) {
        return instances.containsKey(name) || factories.containsKey(name);
    }
}

// Usage
class DatabaseConnection {
    String query(String sql) { return "Result: " + sql; }
}

class CacheClient {
    String get(String key) { return "cached-" + key; }
}

Registry.register("db", new DatabaseConnection());
Registry.registerFactory("cache", CacheClient::new);

DatabaseConnection db = Registry.get("db");
CacheClient cache = Registry.get("cache");

JavaScript

class Registry {
  static #instances = new Map();
  static #factories = new Map();

  static register(name, instance) {
    Registry.#instances.set(name, instance);
  }

  static registerFactory(name, factory) {
    Registry.#factories.set(name, factory);
  }

  static get(name) {
    if (Registry.#instances.has(name)) {
      return Registry.#instances.get(name);
    }
    if (Registry.#factories.has(name)) {
      const instance = Registry.#factories.get(name)();
      Registry.#instances.set(name, instance);
      return instance;
    }
    throw new Error(`No registration found for: ${name}`);
  }

  static has(name) {
    return Registry.#instances.has(name) || Registry.#factories.has(name);
  }
}

// Usage
class DatabaseConnection {
  query(sql) { return `Result: ${sql}`; }
}

class CacheClient {
  get(key) { return `cached-${key}`; }
}

Registry.register('db', new DatabaseConnection());
Registry.registerFactory('cache', () => new CacheClient());

const db = Registry.get('db');
const cache = Registry.get('cache');
console.log(db.query('SELECT 1'));

Explanation

The Registry Pattern has three roles:

  • Registry: The central map that holds instances and factory functions
  • Registration: Code that adds services to the registry at startup
  • Lookup: Code that requests services from the registry at runtime

Instances can be registered directly or lazily created via factories on first access.

Variants

VariantLookup StyleUse Case
Class RegistryRegistry.get("name")Simple string-based lookup
Typed Registryregistry.get(DatabaseConnection.class)Type-safe with generics
Hierarchical RegistryParent fallback chainChild registries override parent defaults
Event Registryon(event, handler)Event bus / pub-sub systems

What Works

  • Register at startup, not during requests. Runtime registration causes race conditions and unpredictable behavior.
  • Use factory registration for expensive objects. Lazy creation avoids startup delays for services that may not be used.
  • Document registry entries. A shared registry without documentation becomes a mystery box.
  • Prefer DI for new code. Registries are pragmatic; DI frameworks are cleaner for large codebases.
  • Avoid runtime mutation. Unregistering or re-registering during operation causes subtle bugs.

Common Mistakes

  • Registry as global variable makes testing difficult. Inject the registry or use a testable wrapper.
  • String-typed lookups are brittle. Rename a service key and every consumer breaks silently.
  • Circular dependencies in the registry cause stack overflows during factory resolution.
  • Storing mutable state in registry entries turns the registry into a hidden global variable.
  • No cleanup on shutdown leaves database connections and file handles open.

Real-World Examples

Django Settings

Django’s settings object is a registry of configuration values. Modules import from django.conf import settings instead of passing config through every function.

WordPress Plugin API

add_action and add_filter register callbacks in a global registry. Themes and plugins hook into WordPress without modifying core files.

JDBC DriverManager

DriverManager.getConnection(url) is a registry that looks up the appropriate database driver based on the URL prefix.

Advanced Topics

Scenario: Registry for Dynamic Plugins

// Registry: store and retrieve implementations by key
interface Plugin {
  name: string;
  init(config: unknown): Promise<void>;
  execute(input: unknown): Promise<unknown>;
}

class PluginRegistry {
  private plugins = new Map<string, Plugin>();
  private factories = new Map<string, () => Plugin>();

  register(plugin: Plugin): void {
    if (this.plugins.has(plugin.name)) throw new Error(`Plugin ${plugin.name} already registered`);
    this.plugins.set(plugin.name, plugin);
  }

  registerFactory(name: string, factory: () => Plugin): void {
    this.factories.set(name, factory);
  }

  get(name: string): Plugin | undefined {
    if (this.plugins.has(name)) return this.plugins.get(name);
    const factory = this.factories.get(name);
    if (factory) {
      const plugin = factory();
      this.plugins.set(name, plugin);
      return plugin;
    }
    return undefined;
  }

  list(): string[] { return [...this.plugins.keys(), ...this.factories.keys()]; }
  unregister(name: string): void {
    this.plugins.delete(name);
    this.factories.delete(name);
  }
}

// Usage: register export plugins
const registry = new PluginRegistry();
registry.register({ name: "csv", init: async () => {}, execute: async (data) => toCSV(data) });
registry.register({ name: "json", init: async () => {}, execute: async (data) => JSON.stringify(data) });
registry.registerFactory("pdf", () => new PDFExportPlugin());

// Client: get plugin by name
const exporter = registry.get("csv");
if (exporter) {
  await exporter.init({ delimiter: "," });
  const result = await exporter.execute(data);
}

console.log(registry.list()); // ["csv", "json", "pdf"]

Lessons:

  • Registry stores implementations by key (name)
  • Lazy registration: factories create the plugin on demand
  • The client gets plugins by name without knowing the class
  • Ideal for plugin systems, extensions, strategies
  • Registry vs Service Locator: Registry is for types; SL is for dependencies
  • In tests, unregister between suites to avoid contamination

### Registry vs Service Locator: which do I use?

Registry is a catalog of implementations: the client asks by name and gets an instance. Service Locator is a dependency resolver: the service asks the locator for its deps. Registry is for plugins/strategies: the client decides which to use. SL is for DI: the service does not know its concrete deps. Registry is explicit: the client calls get("csv"). SL is implicit: the service calls locator.get(Logger). Prefer Registry for extensions, SL only in legacy.

Frequently Asked Questions

What is the difference between Registry and Service Locator?
Service Locator is a specific type of registry focused on resolving service dependencies. Registry is broader and can store any shared object.
Is Registry an anti-pattern?
Some consider it a lighter anti-pattern than global variables, but it shares the same risks. Use it sparingly and prefer DI for new code.
How do I test code that uses a Registry?
Clear the registry before each test, register mocks, and run the test. Better yet, refactor to accept dependencies via constructor injection.