Press enter or click to view image in full size

The Registry Pattern — Simplifying access to commonly used objects

Centralize object management to avoid redundant creation and simplify access

Abu Jobaer

Note: This publication demonstrates the Registry Pattern in Java.

Intent

The Registry provides a well-known interface to register and access commonly used objects. It allows storing globally accessible objects in a centralized location so that any part of the application can access them without requiring direct references.

This implementation is similar to a Service Locator, but its primary purpose is object registration and controlled global access, not replacing proper dependency injection in complex applications.

A scenario

You’re a software engineer working on an e-commerce application. The core application doesn’t know ahead of time when different modules will need access to commonly used services such as a logger, configuration loader, database connector, or analytic services.

Despite this, the application still works because each module manually receives these services through parameters, constructors, or direct imports. This leads to multiple problems:

  • Tight Coupling: Object creation is mixed with client logic.
  • Hard to Test: Directly created objects are hard to replace or mock.
  • Hidden Dependencies: It’s hard to understand what a class actually depends on.
  • Maintenance Nightmare: Changing one service may break unrelated modules.
  • Violation of SRP: Clients end up creating services instead of using them.
  • Spreading Dependencies: Passing services through multiple layers adds complexity.

The new requirement is to create a solution that allows the application to access objects and services without passing them around manually or instantiating them everywhere.

So how would you do that?

Your task

The application already includes multiple services such as a logger, configuration manager, database connector, and plugin system.

Now you need to design a solution that allows the application to access these services without passing them around manually or instantiating them everywhere.

That’s exactly where the Registry Pattern provides an effective solution.

How the Registry Pattern works

When you use the Registry Pattern:

  • You create a central registry object.
  • You store object references inside it using a key.
  • Any part of your system can look up an object by its key.

The registry becomes a controlled global access point — unlike random global variables, it enforces structure, consistency, and safety.

For thread-safe applications, consider using ConcurrentHashMap or synchronizing access to the registry.

Participants

Now, look at the participants that are needed to implement the Registry Pattern.

Registry: Registry — an interface or abstract class.

  • Defines an interface for registering and accessing objects.

Concrete Registry: ServiceRegistryImpl — concrete implementation of the Registry interface.

  • Implements operations for registering, retrieving, and checking objects.
  • Stores references to objects using identifiers.
  • Ensures consistent access to registered objects.

Service Objects: Logger, ConfigManager, DBConnection etc.

  • The actual objects that need to be accessed consistently.
  • Register themselves with the ServiceRegistryImpl.
  • Can be accessed from anywhere in the application.

Client: Any class that needs a service.

  • Retrieves objects from the concrete registry — ServiceRegistryImpl — when needed.
  • Does not need to know how objects are created or where they come from.

The registry lifecycle should be carefully managed — services should ideally be registered during application initialization to avoid runtime surprises.

Let’s visualize how the Registry Pattern works with the scenario we’ve described above.

The UML diagram of Registry Pattern

When a service needs to be accessed, the code simply asks the registry for it by name. The registry returns the requested service, making it available wherever needed in the application.

Probe

Let’s examine the scenario based on the attributes of the Registry Pattern:

  1. A single point of access
    All commonly used objects live in a single ServiceRegistryImpl.
    — Clients fetch what they need from one source, avoiding scattered instantiation or imports.
  2. Decoupling object creation from object usage
    Clients don’t need to know how a service — Logger, ConfigManager, or DBConnection— is created.
    — They only need to know how to fetch it from the ServiceRegistryImpl.
  3. Reducing manual dependency management
    — Reduces passing objects through constructors or parameters.
    — Any class can retrieve the service it needs directly from the ServiceRegistryImpl.
  4. Making the system modular and extensible
    If a service changes, you update it in one place — ServiceRegistryImpl— and everything else continues to work.
    — The client modules only talk to the registry — ServiceRegistryImpl—never directly to each other.
  5. Drawbacks
    — Introduces global state, which can hide dependencies.
    — May be harder to test if not carefully designed.
    — Overwriting objects in the registry can lead to unexpected behavior.
    — Consider alternatives such as Dependency Injection (DI) in complex applications.

Implementation

To implement the Registry Pattern, we begin by defining the Registry interface. This interface establishes the core contract that any concrete registry must fulfill to ensure consistent behavior across different implementations.

interface Registry {
Object get(String key);

void set(String key, Object value);

boolean has(String key);
}

Note: Using Object types requires casting when retrieving objects. For type safety, consider generics.

This interface defines the essential operations for registering, accessing, and verifying the presence of objects within the registry. With the contract in place, the next step is to implement a concrete Registry. Let’s do that:

class ServiceRegistryImpl implements Registry {
private final Map<String, Object> items = new ConcurrentHashMap<>();

@Override
public Object get(String key) {
return items.get(key);
}

@Override
public void set(String key, Object value) {
items.put(key, value);
}

@Override
public boolean has(String key) {
return items.containsKey(key);
}
}

The set(key) method registers an object in the registry with a given name, and the get(key) method retrieves it. The has(key) method checks if a service exists.

Now, we have a registry for registering and accessing services. Let’s create some service objects that will be registered:

class DBConnection {
private final String connectionString;

public DBConnection(String connectionString) {
this.connectionString = connectionString;
}

public void connect() {
System.out.println("Connected database: " + this.connectionString);
}
}

class Logger {
public void log(String message) {
System.out.println("[LOG]: " + message);
}
}

class ConfigManager {
private final Map<String, String> config = new HashMap<>();

public void set(String key, String value) {
config.put(key, value);
}

public String get(String key) {
return config.getOrDefault(key, "");
}
}

These are the service objects that will be stored in the Registry so they can be accessed from anywhere in the application.

It’s time to register these services to the Registry. First, let’s create the services we want to share across the application:

DBConnection database = new DBConnection("localhost:5432/myapp");
Logger logger = new Logger();
ConfigManager config = new ConfigManager();

Now we register them:

AppRegistry.register("database", database);
AppRegistry.register("logger", logger);
AppRegistry.register("config", config);

To make this easy to use, we add a small wrapper class called AppRegistry for ServiceRegistryImpl. This wrapper exposes static methods so any part of the application can interact with the registry without needing an instance:

class AppRegistry {

private static final Registry registry = new ServiceRegistryImpl();

public static void register(String key, Object value) {
registry.set(key, value);
}

public static Object get(String key) {
return registry.get(key);
}

public static boolean has(String key) {
return registry.has(key);
}
}

Note: The static wrapper provides global access, which is convenient but should be used carefully to avoid hidden dependencies.

Now any part of the application can access these services. The following example shows how different modules of the application — such as controllers — can access shared services through the AppRegistry, without needing to know how these services are created.

class UserController {

public void createUser(String name) {
Logger logger = (Logger) AppRegistry.get("logger");
DBConnection database =
(DBConnection) AppRegistry.get("database");

logger.log("Creating user: " + name);

database.connect();

// Process user logic here
}
}

Here, UserController fetches the Logger and DBConnection from the registry. Notice that it doesn’t create these services itself.

class OrderController {

public void processOrder(String orderId) {
Logger logger = (Logger) AppRegistry.get("logger");
ConfigManager config =
(ConfigManager) AppRegistry.get("config");

logger.log("Processing order: " + orderId);

String setting = config.get("payment_gateway");

// Process order logic here
}
}

Similarly, OrderController fetches the Logger and ConfigManager. The controller only focuses on business logic — the registry handles service access.

Finally, let’s see a sample of client usage:

public class Main {
public static void main(String[] args) {

DBConnection database = new DBConnection("localhost:5432/myapp");
Logger logger = new Logger();
ConfigManager config = new ConfigManager();

AppRegistry.register("database", database);
AppRegistry.register("logger", logger);
AppRegistry.register("config", config);

UserController userController = new UserController();
userController.createUser("John");
// Outputs:
// [LOG]: Creating user: John
// Connected to database: localhost:5432/myapp

OrderController orderController = new OrderController();
orderController.processOrder("12345");
// Outputs:
// [LOG]: Processing order: 12345
}
}

This demonstrates that the Registry provides a single, centralized point of access for services. Both controllers can use shared resources without passing them through constructors or manually instantiating objects.

Conclusion

The Registry Pattern is useful for simplifying access to shared objects in modular systems and plugin-based applications.

It provides centralized control, decoupling, and extensibility, but also introduces global state and potential hidden dependencies.

For large-scale or highly testable systems, consider Dependency Injection frameworks instead of relying solely on the Registry Pattern. However, in dynamic or plugin-based applications, the Registry remains a practical solution.

Since you’ve made it this far, hopefully you enjoyed the reading! Please share the article.

Follow me on
Medium, Twitter, and Github