patron-service-locator

Service Locator: Why It's Often an Anti-Pattern

  • 5 min

A Service Locator is a registry that an object queries to obtain its dependencies. It simplifies access but hides those requirements within the class body.

In the previous article, we saw that Dependency Injection (DI) is the elegant way for our objects to get what they need: they ask for it in the constructor, and someone provides it.

But there is another way to obtain dependencies. A more “self-sufficient” way, where the object, instead of asking, searches for what it needs in a central registry.

This technique is called Service Locator.

Although it solves the same problem as DI (decoupling concrete classes), the way it does so has dangerous side effects. So much so that today, most software architects consider it an Anti-Pattern.

What is a Service Locator?

A Service Locator is a central registry that holds references to services and allows clients to request them.

Imagine a Yellow Pages or a Concierge Desk. If you need a plumber, you don’t wait for someone to bring one to your home (DI). You go to the concierge and say: “Hey, give me the plumber’s contact.”

Implementation in C#

Let’s see what a simple implementation looks like.

// The Central Registry
public static class ServiceLocator
{
    private static readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();

    // Register a service (done at application startup)
    public static void Register<T>(T service)
    {
        _services[typeof(T)] = service;
    }

    // Get a service (used by the client)
    public static T Get<T>()
    {
        try 
        {
            return (T)_services[typeof(T)];
        }
        catch (KeyNotFoundException)
        {
            throw new Exception($"The service {typeof(T).Name} has not been registered.");
        }
    }
}
Copied!

Now, let’s see how a client class uses it. Notice the difference from Dependency Injection:

public class OrderManager
{
    private ILogger _logger;
    private IDatabase _db;

    public OrderManager()
    {
        // ❌ The constructor has no parameters,
        // but internally it is ACTIVELY SEARCHING for dependencies.
        _logger = ServiceLocator.Get<ILogger>();
        _db = ServiceLocator.Get<IDatabase>();
    }

    public void Process()
    {
        _logger.Log("Processing order...");
        _db.Save();
    }
}
Copied!

Why is it Considered an Anti-Pattern?

At first glance, it seems convenient. We don’t have constructors with 5 parameters. We simply call Get<T>() wherever we want. But this convenience comes at a high cost.

It Hides Dependencies (Lying APIs)

This is the biggest sin. If you look at the class definition:

public class OrderManager 
{
    public OrderManager() { ... }
}
Copied!

The constructor tells us: “Hello, I am OrderManager and I don’t need anything to work.” That’s a lie!

If you try to instantiate this class without previously configuring the global locator, the application will fail at runtime. Dependency injection makes these requirements explicit in the constructor.

It Makes Unit Tests Difficult

The Service Locator is often a static class (Singleton). Static state is the enemy of testing.

  • If one test registers a “Mock Database” in the Locator, that state persists.
  • The next test might fail because it expects a real database but finds the Mock from the previous test.
  • You cannot run tests in parallel.

Runtime Errors vs. Compilation Errors

With constructor-based Dependency Injection, if you forget to pass a parameter, the code does not compile. The compiler warns you.

With Service Locator, the code compiles perfectly. The error will appear on Friday at 6:00 PM in production when the code tries to retrieve a service that no one registered.

When SHOULD You Use Service Locator?

Despite all the hate it receives, the Service Locator is not useless. It has its niches where it shines or is necessary:

Game Development

In engines like Unity, the Service Locator pattern is ubiquitous (e.g., GetComponent<RigidBody>()). Some engines and frameworks offer their own registries or contexts. It’s advisable to encapsulate its use at the integration boundary; creating thousands of objects doesn’t inherently make constructor injection a performance issue.

Refactoring Legacy Code

If you have a monstrous legacy application, switching to Dependency Injection all at once can be impossible. Service Locator can serve as an intermediate step to start decoupling classes without breaking the entire existing instantiation system.

Extending Functionality

Sometimes you extend a framework that controls object creation and you cannot change the constructor. In these cases, a scoped locator can serve as a bridge to the application’s services.

Service Locator vs Dependency Injection

To conclude, a quick comparison to clear up any doubts:

FeatureDependency Injection (DI)Service Locator (SL)
PhilosophyThe object receives what it needs.The object searches for what it needs.
DependenciesExplicit (in the constructor).Hidden (inside the code).
TestabilityHigh (easy to mock).Low (global static state).
ReadabilityYou see what the class needs at a glance.You have to read the code to know what it uses.

Service Locator is like fast food: convenient and easy at first, but if you overuse it, your health (the code’s health) will suffer in the long run.

Whenever you can, use Dependency Injection. Leave Service Locator only for very specific cases where you have no control over the object’s lifecycle or the framework forces you to use it.