Design patterns comparison

Dependency Injection vs Service Locator

Both patterns answer the same question: how should a component obtain the objects it depends on? Dependency Injection has the dependencies pushed in from outside. Service Locator has the component pull them out of a central registry. That single difference has profound consequences for testability, readability, and coupling — and it is one of the most debated distinctions in software design.

TL;DR

  • Dependency Injection (DI) — dependencies are passed to a component by its caller, typically via the constructor. The component declares what it needs; the outside world supplies it. Dependencies are explicit.
  • Service Locator — a component calls a central registry at runtime to retrieve its dependencies. Dependencies are implicit — hidden inside the implementation body.
  • Both implement Inversion of Control (IoC) — neither pattern has the component creating its own dependencies with new. Control over construction is inverted to an external party.
  • Prefer DI in almost all new application code. Service Locator is appropriate in specific framework and plugin scenarios only.

Side-by-side comparison

AspectDependency InjectionService Locator
How dependencies are obtained Passed in — via constructor, method parameter, or property Looked up from a registry inside the component
Transparency High — dependencies visible in the constructor signature Low — dependencies hidden inside the method body
Testability High — pass a mock or stub in tests via the constructor Lower — tests must configure the global registry before each run
Coupling Component depends only on its interfaces, not on the container Component is coupled to the locator itself
Compile-time safety Better — missing dependency causes a compile or startup error Weaker — missing registration typically only fails at runtime
Hidden dependencies None — all requirements declared in the interface Common — callers cannot tell what a component needs without reading it
Framework examples Spring, .NET DI, Angular, NestJS, InversifyJS Legacy frameworks, plugin registries, some CMS systems
Typical use case Application services, repositories, controllers, use cases Plugin systems, framework internals, legacy migration stepping stone

What is Dependency Injection?

Dependency Injection is a technique in which a component receives the objects it depends on rather than creating them itself. The most common form is constructor injection: every required dependency is listed as a constructor parameter. When something wants to create the component, it must supply those dependencies — and the component is then fully initialised and ready to use.

This approach follows the Explicit Dependencies Principle: a method or class should honestly declare all the things it requires to do its job. Constructor injection makes this requirement part of the type signature, so violations are caught by the compiler or at application startup rather than in production.

A DI container (also called an IoC container) automates the wiring. You register your types once at the composition root — the single point where the object graph is assembled — and the container resolves the full dependency tree for you. Spring's ApplicationContext, .NET's IServiceCollection, and Angular's hierarchical injector are all examples of DI containers.

Constructor injection (TypeScript)

{`// Dependencies declared in the constructor signature.
// Any caller must supply them — nothing is hidden.
class OrderService {
  constructor(
    private readonly orderRepo: IOrderRepository,
    private readonly emailService: IEmailService,
    private readonly logger: ILogger,
  ) {}

  async placeOrder(cart: Cart): Promise<Order> {
    const order = await this.orderRepo.save(cart);
    await this.emailService.sendConfirmation(order);
    this.logger.info('Order placed', { id: order.id });
    return order;
  }
}

// Unit test — pass mocks directly, no global setup
const svc = new OrderService(
  mockRepo, mockEmail, mockLogger,
);`}

NestJS DI (decorator-based)

{`// NestJS uses TypeScript decorators
// and a module-level DI container.
import { Injectable } from '@nestjs/common';

@Injectable()
export class OrderService {
  constructor(
    private readonly orderRepo: OrderRepository,
    private readonly emailService: EmailService,
  ) {}

  async placeOrder(cart: Cart): Promise<Order> {
    const order = await this.orderRepo.save(cart);
    await this.emailService.sendConfirmation(order);
    return order;
  }
}
// The container resolves OrderRepository
// and EmailService automatically.`}

What is Service Locator?

The Service Locator pattern uses a central registry that components can query to retrieve their dependencies by name or type. Rather than being given its dependencies, a component calls the locator — typically a singleton — and asks for what it needs.

The pattern became well-known through Mark Seemann's 2010 post "Service Locator is an Anti-Pattern", which argued that it violates the Explicit Dependencies Principle and should generally be avoided in application code. His argument: the locator makes dependencies invisible, so the class's public interface lies about what it needs.

However, Service Locator is not universally wrong. Frameworks themselves often use a registry internally. Plugin systems — where one module must discover services registered by another module it was not compiled against — have a genuine need for runtime lookup. The key distinction is whether consuming application code calls the locator directly.

Service Locator — registry lookup

{`// Dependencies fetched from a global registry
// inside the method — hidden from the caller.
class OrderService {
  async placeOrder(cart: Cart): Promise<Order> {
    const repo = ServiceLocator
      .get<IOrderRepository>('OrderRepository');
    const email = ServiceLocator
      .get<IEmailService>('EmailService');
    const logger = ServiceLocator
      .get<ILogger>('Logger');

    const order = await repo.save(cart);
    await email.sendConfirmation(order);
    logger.info('Order placed', { id: order.id });
    return order;
  }
}

// Test: must configure the registry first
ServiceLocator.register('OrderRepository', mockRepo);
ServiceLocator.register('EmailService', mockEmail);`}

Register / resolve pattern (Java)

{`// A minimal Service Locator implementation
public class ServiceLocator {
  private static final Map<Class<?>, Object>
    registry = new HashMap<>();

  public static <T> void register(
    Class<T> type, T impl) {
    registry.put(type, impl);
  }

  @SuppressWarnings("unchecked")
  public static <T> T resolve(Class<T> type) {
    T svc = (T) registry.get(type);
    if (svc == null) throw new RuntimeException(
      "No registration for " + type.getName());
    return svc;
  }
}

// Usage (inside a component — avoid this)
IOrderRepository repo =
  ServiceLocator.resolve(IOrderRepository.class);`}

Spring vs Angular DI — two mainstream approaches

Both Spring (Java/Kotlin) and Angular (TypeScript) are built on the DI principle but implement it differently.

Spring uses annotations on classes (@Component, @Service, @Repository) and injection points (@Autowired, constructor injection). The Spring ApplicationContext scans the classpath, builds a bean registry, and resolves the dependency graph at startup. Spring Boot's auto-configuration reduces boilerplate significantly.

Angular uses a hierarchical injector that mirrors the component tree. Each @NgModule and each component can provide its own injector scope, so a service can be application-wide (provided in root) or scoped to a feature module. The @Injectable() decorator marks a class as injectable, and dependencies are declared in the constructor — same principle as constructor injection everywhere else.

The practical difference: Spring has a flat, application-wide container by default; Angular's hierarchy means a service instance can be shared across one subtree but not others — useful for feature-level state. If you work with either framework, understanding these differences matters for interviews and code reviews. See also TypeScript vs JavaScript for context on Angular's type system.

How engineers talk about DI vs Service Locator

These are the phrases you will hear in code reviews, architecture discussions, and technical interviews:

Dependency Injection

  • "We inject the repository via the constructor — makes it trivial to swap in a mock for tests."
  • "The IoC container wires everything together at startup. Application code never calls the container directly."
  • "This class has too many constructor parameters — it's probably violating the Single Responsibility Principle."
  • "We're using constructor injection rather than property injection to ensure a fully initialised object at all times."
  • "The composition root is the one place where we configure the container — nothing else should reference it."

Service Locator & IoC

  • "This is a hidden dependency — you can't tell from the constructor what the class actually needs."
  • "The component is pulling from the locator instead of having its dependencies pushed in — that's why it's hard to unit test."
  • "We want to move away from the global registry and towards explicit constructor injection everywhere."
  • "The Hollywood Principle — don't call us, we'll call you — that's the core idea behind IoC."
  • "This is appropriate in a plugin system, where the plugin can't know at compile time what services are available."

When to use each pattern

Use Dependency Injection when:

  • Building application services, repositories, or controllers. Constructor injection makes requirements explicit, keeps components testable, and is what every major framework expects.
  • You want fast, isolated unit tests. Pass a mock directly via the constructor — no global state, no configuration overhead.
  • Using a DI framework. Spring, Angular, NestJS, and .NET's built-in container all prescribe constructor injection. Follow their conventions.
  • You want compile-time or startup-time safety. Typed DI containers catch missing registrations before a request ever arrives, not in production at 3 am.
  • Code readability matters. A constructor signature is a contract. Readers know immediately what a class needs without opening the body.

Service Locator may be appropriate when:

  • Building a plugin or extension system. Plugins registered at runtime cannot be known at compile time; a registry is the natural solution.
  • Working inside framework internals. The framework owns the registry and its consumers are carefully controlled — a different situation from application code.
  • Migrating legacy code incrementally. Introducing a locator can be a transitional step away from new Dependency() calls everywhere — but treat it as temporary scaffolding, not a destination.

Quick decision guide

  • Building a new service, repository, or controller → Constructor injection
  • Need fast, isolated unit tests → Constructor injection
  • Using Spring, NestJS, Angular, or .NET DI → follow the framework's DI conventions
  • Seeing hidden dependencies inside method bodies → refactor to constructor injection
  • Building a plugin system with runtime service discovery → Service Locator may be appropriate
  • Legacy codebase full of new Dependency() calls → introduce a DI container incrementally
  • Framework internals that own their registry → Service Locator is acceptable inside the framework boundary

Key vocabulary

  • Dependency Injection (DI) — a design pattern in which a component's dependencies are supplied by an external caller rather than created inside the component.
  • Constructor injection — the most common DI form: dependencies are passed as constructor parameters, ensuring a fully initialised object.
  • Property injection — dependencies set via public properties after construction; less safe because it allows partially initialised objects.
  • Service Locator — a pattern in which components retrieve their dependencies by querying a central registry at runtime.
  • IoC (Inversion of Control) — the principle that a component's dependencies are managed externally rather than by the component itself.
  • IoC container / DI container — a framework component that automates dependency injection by maintaining a registry and resolving dependency trees.
  • Composition root — the single location in an application where the object graph is assembled; the only place allowed to reference the DI container directly.
  • Hidden dependency — a dependency that a component requires but does not declare in its public interface, typically because it is fetched from a global registry inside the body.
  • Explicit dependency — a dependency declared in a constructor or method signature, making the requirement visible to callers and the compiler.
  • Register / resolve — the two operations of a Service Locator: registering an implementation against a key, and resolving (retrieving) it later by that key.
  • Hollywood Principle — "Don't call us, we'll call you"; the informal name for Inversion of Control.
  • Testability — how easily a component can be tested in isolation; DI dramatically improves testability by making dependencies swappable.

Frequently asked questions

Is Dependency Injection always better than Service Locator?

DI is the preferred pattern in the vast majority of modern codebases because it makes dependencies explicit, improves testability, and keeps components decoupled from the infrastructure that wires them together. Service Locator is sometimes appropriate in legacy systems, framework internals, and plugin architectures where components must discover dependencies dynamically at runtime. It is not inherently wrong, but the hidden dependencies it creates make code harder to understand and test. The general advice: prefer DI; reach for Service Locator only when dynamic lookup is genuinely required.

What is an IoC container?

An IoC (Inversion of Control) container is a framework component that automates dependency injection. Rather than constructing objects manually and passing their dependencies by hand, you register types and their dependency rules with the container, and it builds objects for you on demand — resolving the entire dependency tree automatically. Examples include Spring (Java/Kotlin), .NET's built-in DI container, Angular's injector, NestJS's module system, and InversifyJS (TypeScript). The container is itself a Service Locator internally, but consuming code uses constructor injection and never calls the container directly.

What is the Hollywood Principle?

"Don't call us, we'll call you." The Hollywood Principle describes Inversion of Control: instead of your component calling a framework or container to obtain dependencies (Service Locator style), the framework calls your component and passes the dependencies in (DI style). The name is a humorous analogy to film auditions — the studio contacts actors when needed, not the other way round.