Skip to content
Concepts

What Is Inversion of Control? Frameworks, Callbacks, Dependency Injection and Containers

Inversion of control is what happens when someone else's code decides when yours runs, or what it works with. Here is what the term means, where it came from, the forms it takes, from callbacks and event loops to the template method and dependency injection, how an IoC container works inside, with runnable examples, how it differs from the dependency inversion principle, and when it is worth it.

A web framework calls a function when a request arrives. A test runner finds the tests in a file and runs each one. A browser calls a handler when a button is clicked, and an application server hands a class the database connection it asked for in its constructor. In each case the code a programmer wrote does not decide when it runs or where its collaborators come from: something else does. That reversal has a name, inversion of control, and it is one of the ideas most of modern software is quietly built on. This article explains what it means, where the term came from, the forms it takes, how dependency injection and IoC containers work inside, and when the indirection is worth paying for. It closes a series on SOLID: where the dependency inversion principle says which way dependencies should point, this is the mechanism that makes them point that way at run time. Every output shown here was produced by running the code on Windows 11, with Python 3.14.6 and with the .NET 10 SDK and Microsoft.Extensions.DependencyInjection 10.0.13, not written by hand.

What inversion of control is

Inversion of control (IoC) is the arrangement in which a piece of reusable code, usually a framework, calls the code a programmer writes, instead of the programmer’s code calling it. The control flow, who decides what runs next, belongs to the framework, and the programmer’s code is plugged into it.

The clearest way to see it is to compare a library with a framework. A library is a set of functions a program calls; each call does some work and returns control to the caller. A framework embodies a design of its own: the programmer fills in pieces, and the framework calls those pieces at the right moments. Martin Fowler names that inversion as the very thing that distinguishes a framework from a library.1 Ralph Johnson and Brian Foote described it in 1988: the methods a user writes to tailor a framework are often called from inside the framework rather than from the user’s own code, so the framework, not the application, plays the role of the main program.2

Who calls whom. On the left, a program using a library: the program's own main loop is in charge and calls library functions such as parse, format and send whenever it decides to, and each call returns. On the right, a program built on a framework: the framework owns the main loop, and it calls the program's handlers, such as on request, on click and on test, when events happen. The arrows of control point the other way: that reversal is inversion of control.Using a libraryyour code is in chargeYour programwhile running: decide, callparse()format()send()library functionscallscallscallsBuilding on a frameworkthe framework is in chargeFrameworkevent loop, router, test runneron_requeston_clicktest_totalyour handlerscallscallscalls
With a library your code is in charge and calls out; with a framework the framework is in charge and calls in. Same pieces, opposite arrows.

A few lines of Python show the difference. The first function is in charge: it asks for each value in turn and decides what happens next. The second half hands the loop to a tiny framework, registers two handlers, and lets the framework call them as events arrive:

python
from collections import defaultdict


# Your code in charge: it asks for each input and decides what happens next.
def ask_loop(answers):
    replies = iter(answers)
    name = next(replies)
    print(f"[program] asked for a name, got {name!r}")
    colour = next(replies)
    print(f"[program] asked for a colour, got {colour!r}")
    print(f"[program] {name} likes {colour}")


# The framework in charge: it owns the loop and calls whatever was registered.
class TinyFramework:
    def __init__(self):
        self.handlers = defaultdict(list)

    def on(self, event):
        def register(handler):
            self.handlers[event].append(handler)
            return handler
        return register

    def run(self, events):
        for event, payload in events:
            for handler in self.handlers[event]:
                print(f"[framework] {event} -> {handler.__name__}")
                handler(payload)


app = TinyFramework()
profile = {}


@app.on("name")
def remember_name(value):
    profile["name"] = value


@app.on("colour")
def remember_colour(value):
    profile["colour"] = value
    print(f"[handler] {profile['name']} likes {value}")


ask_loop(["Ada", "green"])
print()
app.run([("name", "Ada"), ("colour", "green"), ("size", 42)])
txt
[program] asked for a name, got 'Ada'
[program] asked for a colour, got 'green'
[program] Ada likes green

[framework] name -> remember_name
[framework] colour -> remember_colour
[handler] Ada likes green

Both halves do the same work, but the second one never says when anything happens. The handlers only say what to do if their event comes, and the size event, which nobody registered for, passes without a word. That is the trade at the heart of inversion of control: the programmer gives up the main loop, and in return gets one that someone else has written, tested and made to handle the cases the programmer would rather not think about.

Don’t call us, we’ll call you

The idea is often summed up as the Hollywood principle: don’t call us, we’ll call you. The phrase comes from Richard Sweet’s paper on the Mesa programming environment at Xerox PARC, where “Hollywood’s Law” was a design goal for the tools of the environment: rather than asking the user for a command and then executing it, a tool should let the system notify it of the user’s actions.31 It is the same reversal: the tool does not poll, it waits to be called.

A short history

The 1980s: frameworks and the inverted main program

The behaviour is older than the name. Any program built on an interrupt handler, an operating system’s callbacks or a windowing system’s event loop has handed some control to code it did not write. What changed in the 1980s was that object-oriented frameworks made the arrangement deliberate. Sweet’s Mesa paper, published in 1985, made “don’t call us, we’ll call you” an explicit design goal for the tools of a programming environment.3 Johnson and Foote’s 1988 article “Designing Reusable Classes” described frameworks, sets of classes that embody an abstract design for a family of related problems, and observed that the framework, by calling the methods its users supply, takes over the role of the main program. Fowler traces the term inversion of control to that paper.21

The 1990s: patterns and principles

In 1994 the Design Patterns book by Gamma, Helm, Johnson and Vlissides catalogued the Template Method, a base class whose algorithm calls steps that subclasses fill in: inversion of control within a single class hierarchy.4 In 1996 Robert C. Martin published the dependency inversion principle in his column for The C++ Report, a rule about which way dependencies between modules should point.5 Despite the similar name it is a different idea, with an article of its own, and the two are compared below.

The 2000s: containers and dependency injection

Around the turn of the century the Java world brought the term into everyday use, as a reaction against the heavyweight component model of the time. The Apache Avalon project, proposed in January 1999, made inversion of control a central design principle of its component framework, with one of its founders, Stefano Mazzocchi, promoting the concept.6 In 2002 Rod Johnson’s book Expert One-on-One J2EE Design and Development shipped with about 30,000 lines of framework code that already contained an IoC container; that code became the Spring Framework, which went public in June 2003 and reached version 1.0 in March 2004.78 PicoContainer, a small container that built objects through their constructors, released its 1.0 in June 2004 after about a year of work.910

By then inversion of control had become the name of a whole category of “lightweight containers”, and Martin Fowler argued in January 2004 that the name was too generic: every framework inverts control in some way, so saying a container did so said little. The particular inversion these containers performed was about how a component gets its dependencies, and after discussion with the advocates of IoC he proposed a more specific name, dependency injection.10 The name stuck.

In March 2007 Google released Guice, its internal Java dependency injection framework, as open source, with Bob Lee leading the project.11 Lee and Rod Johnson then led JSR 330, which standardised a common set of annotations for Java dependency injection, including @Inject, and reached its final release in October 2009.1213

Today: built into the platform

Dependency injection is now part of the platforms themselves rather than an add-on. .NET ships a container in Microsoft.Extensions.DependencyInjection, and describes dependency injection as a technique for achieving inversion of control between classes and their dependencies.14 Angular builds it into the framework: components and services ask for what they need, and a hierarchy of injectors supplies it.1516 Spring’s reference documentation still introduces its core as the IoC container, and calls dependency injection a specialised form of inversion of control.17

A timeline at a glance

YearEvent
1985Sweet’s Mesa paper makes “don’t call us, we’ll call you” a design goal for programming tools
1988Johnson and Foote describe frameworks that invert control of the main program
1994Design Patterns catalogues the Template Method
1996Martin publishes the dependency inversion principle
1999Apache Avalon is proposed, with inversion of control as a core principle
2002Rod Johnson’s book ships the code that becomes Spring
2004Fowler names dependency injection; Spring 1.0 and PicoContainer 1.0 are released
2007Google open-sources Guice
2009JSR 330 standardises @Inject for Java

The forms it takes

Inversion of control is a family of techniques rather than one. They share the reversal, someone else’s code deciding when yours runs, and differ in what is handed over.

The family of ideas around inversion of control. Inversion of control is the umbrella: someone else's code decides when yours runs. Under it sit callbacks and event handlers, event loops, the template method pattern, plug-ins and frameworks, and dependency injection, where someone else decides which objects yours works with. Dependency injection can be done by hand in a composition root or by an IoC container. A service locator also hands out dependencies, but the class still asks for them itself. Beside the umbrella, not under it, is the dependency inversion principle, a design rule about which way source code dependencies point; dependency injection is a common way to follow it.Inversion of controlsomeone else decides when your code runsCallbacks,event handlersEvent loopsTemplatemethodFrameworks,plug-insDependency injectionsomeone else decides what withBy handcomposition rootContainerauto-wiringService locatorthe class still asksDependency inversion principlenot a mechanism but a design rule:high- and low-level modules bothdepend on abstractionsdependency injection often serves it
Inversion of control is the umbrella. Dependency injection is the member that hands over the choice of collaborators; the dependency inversion principle is a related design rule, not a member.

Callbacks and event handlers

A callback is a function passed to other code so that the other code can call it later. Sorting with a comparison function, registering a click handler in a browser and passing a completion function to an asynchronous read are all callbacks. The receiver decides when, and how often, the function runs.

Event loops

An event loop is a framework in its smallest form. The program registers callbacks and returns; the runtime waits for events and calls whatever was registered for them. Node.js is built this way: when an I/O operation completes, its callback is placed in a queue, and the loop works through its phases, timers, pending callbacks, polling for I/O and the rest, running the queued callbacks in order.18 The program never writes the loop; it only writes what happens inside it, as TinyFramework above showed.

The template method

The Template Method puts the inversion inside a class hierarchy. A base class defines the skeleton of an algorithm and calls steps that subclasses supply; the subclasses never call the skeleton’s steps themselves, they are called.4

python
from abc import ABC, abstractmethod


class Report(ABC):
    """The skeleton is fixed here; subclasses only fill in the steps."""

    def render(self):  # the template method: subclasses do not override it
        lines = [self.header()]
        lines += [f"  {row}" for row in self.rows()]
        lines.append(self.footer())
        return "\n".join(lines)

    def header(self):
        return f"== {type(self).__name__} =="

    @abstractmethod
    def rows(self): ...

    def footer(self):  # a hook with a default the subclass may replace
        return "-- end --"


class SalesReport(Report):
    def rows(self):
        return ["north: 120", "south: 95"]


class StockReport(Report):
    def rows(self):
        return ["pens: 40"]

    def footer(self):
        return "-- reorder below 50 --"


for report in (SalesReport(), StockReport()):
    print(report.render())
txt
== SalesReport ==
  north: 120
  south: 95
-- end --
== StockReport ==
  pens: 40
-- reorder below 50 --

SalesReport and StockReport contain no control flow at all. They answer two questions, what the rows are and what the footer says, and render decides when to ask. This is Johnson and Foote’s white-box framework: it is reused by subclassing, so its users must know something of its insides. Their black-box frameworks are reused instead by plugging in objects that follow an interface, which is where dependency injection comes from.2

Frameworks and plug-ins

A full framework combines all of these. A test runner is a good example because it is so familiar. Python’s unittest discovers the test files, finds the TestCase subclasses inside them, creates an instance for each test method, and for each one calls setUp, then the test, then tearDown.19 The person who writes the tests never calls any of it. Web frameworks do the same with request handlers, GUI toolkits with widgets and plug-in hosts with plug-ins.

Dependency injection

The last member of the family hands over a different decision. The others give away when code runs; dependency injection gives away which objects it works with. It is the subject of the rest of this article.

Dependency injection

The problem it solves

A class that creates its own collaborators decides, inside itself, exactly which ones it uses. Nothing outside the class can change that decision, and nothing outside it can even see it:

python
from typing import Protocol


class SmtpMailer:
    def send(self, to, text):
        raise ConnectionError("no SMTP server in this sandbox")


# Before: the service picks and builds its own collaborator.
class SignupServiceBefore:
    def __init__(self):
        self.mailer = SmtpMailer()  # fixed here, invisible from outside

    def sign_up(self, email):
        self.mailer.send(email, "Welcome!")
        return f"signed up {email}"


# After: the service states what it needs; whoever builds it decides what.
class Mailer(Protocol):
    def send(self, to: str, text: str) -> None: ...


class SignupService:
    def __init__(self, mailer: Mailer):  # constructor injection
        self.mailer = mailer

    def sign_up(self, email):
        self.mailer.send(email, "Welcome!")
        return f"signed up {email}"


class OutboxMailer:  # a stand-in that only records what it was asked to send
    def __init__(self):
        self.sent = []

    def send(self, to, text):
        self.sent.append((to, text))


try:
    SignupServiceBefore().sign_up("ada@example.com")
except ConnectionError as error:
    print("before:", error)

outbox = OutboxMailer()
service = SignupService(outbox)  # the caller composes the objects
print("after:", service.sign_up("ada@example.com"))
print("outbox:", outbox.sent)
txt
before: no SMTP server in this sandbox
after: signed up ada@example.com
outbox: [('ada@example.com', 'Welcome!')]

SignupServiceBefore cannot be used, or tested, without a mail server, because it builds an SmtpMailer the moment it is created. SignupService does the same job but only states what it needs: something with a send method. The code that creates it decides what that is, a real mailer in production and an outbox that just records the message in a test. The service has not changed; only the decision has moved out of it.

Dependency injection, before and after. Before: the signup service creates its own SMTP mailer with new, so the dependency is fixed inside the class and hidden from anyone using it. After: the signup service only asks, in its constructor, for something that satisfies the Mailer interface. The composition root at the program's entry point decides what that is: the SMTP mailer in production, or an outbox mailer that records messages in a test. Both implement the interface, and the service never names either of them.Beforethe class builds what it needsSignupServiceself.mailer =SmtpMailer()SmtpMailercreatesfixed and hiddenAfterthe class states what it needs; the caller supplies itSignupService__init__(self,mailer: Mailer)Composition rootat the entry point:decides and wirespasses inMailer (interface)SmtpMailerOutboxMailerimplementthe interface
Before, the class creates its collaborator and hides the choice. After, it declares the interface it needs, and the composition root at the entry point decides and passes in the implementation.

That move is the whole of dependency injection. A dependency is an object another object needs to do its work; injecting it means handing it in from outside rather than letting the object find or build it. Fowler’s original description says the same thing in terms of the containers of the time: a separate object, the assembler, populates a field of the class with a suitable implementation.10

Three ways to inject

Fowler distinguished three forms, by how the dependency is handed over:10

FormHow the dependency arrivesAssociated in 2004 with
Constructor injectionAs a constructor argument; the object cannot exist without itPicoContainer
Setter injectionThrough a property or setter method after constructionSpring
Interface injectionThrough a method of an interface the object implements for the purposeAvalon

Constructor injection has become the usual default, because it makes the dependencies part of the object’s signature: an object that has been created at all has everything it needs, and a reader can see what it depends on by looking at one method. Setter injection suits dependencies that are genuinely optional or have a sensible default. Interface injection is rarely seen today.

The composition root

If no class builds its own collaborators, something must build all of them. Mark Seemann’s answer is the composition root: a single place, as close as possible to the application’s entry point, where the whole object graph is put together. The rest of the application only declares what it needs, and only an application, never a library, has a composition root.20 In the example above it is the line SignupService(outbox). In a real application it is a few dozen lines in main or in the start-up code, and it can be written entirely by hand. For a small program, wiring the objects by hand like this, without any container, is often all that is needed.

IoC containers

What a container does

As an application grows, the composition root grows with it, and much of it becomes mechanical: to build a CheckoutService, first build an OrderRepository, which needs a Database, and so on down the graph. An IoC container, or DI container, automates that. It does four things:

  1. Registration. The program tells the container which implementation to use for each service, often an interface: “when someone needs a Mailer, give them a ConsoleMailer”.
  2. Resolution and auto-wiring. Asked for a service, the container looks at the constructor of its implementation, works out what that needs, resolves each of those in turn, and builds the whole graph from the bottom up.
  3. Lifetimes. It decides whether each request gets a new object or a shared one, and how long a shared one lives.
  4. Disposal. It cleans up the objects it created when their lifetime ends.

A container in sixty lines

Nothing about this needs magic. The container below reads constructor type hints to auto-wire, supports the three lifetimes most containers offer, and reports cycles and missing registrations:

python
import inspect
from typing import get_type_hints

TRANSIENT, SCOPED, SINGLETON = "transient", "scoped", "singleton"


class Container:
    def __init__(self):
        self.registrations = {}  # service -> (implementation, lifetime)
        self.singletons = {}

    def register(self, service, implementation=None, lifetime=TRANSIENT):
        self.registrations[service] = (implementation or service, lifetime)

    def scope(self):
        return Scope(self)


class Scope:
    def __init__(self, container):
        self.container = container
        self.instances = {}

    def resolve(self, service, _chain=()):
        if service in _chain:
            path = " -> ".join(s.__name__ for s in (*_chain, service))
            raise LookupError(f"cycle: {path}")
        if service not in self.container.registrations:
            raise LookupError(f"nothing registered for {service.__name__}")

        implementation, lifetime = self.container.registrations[service]
        cache = {SINGLETON: self.container.singletons,
                 SCOPED: self.instances}.get(lifetime)
        if cache is not None and service in cache:
            return cache[service]

        # Auto-wiring: read the constructor's type hints and resolve each one.
        hints = get_type_hints(implementation.__init__)
        arguments = {
            name: self.resolve(hints[name], (*_chain, service))
            for name in inspect.signature(implementation).parameters
        }
        instance = implementation(**arguments)
        if cache is not None:
            cache[service] = instance
        return instance

An application registers its classes once and then only ever asks for the top of the graph:

python
class Clock:
    def now(self):
        return "2026-10-20T12:00"


class Database:
    count = 0

    def __init__(self):
        Database.count += 1
        self.id = Database.count


class Mailer:
    def send(self, to, text):
        print(f"  mail to {to}: {text}")


class ConsoleMailer(Mailer):
    pass


class OrderRepository:
    def __init__(self, db: Database):
        self.db = db


class CheckoutService:
    def __init__(self, orders: OrderRepository, mailer: Mailer, clock: Clock):
        self.orders, self.mailer, self.clock = orders, mailer, clock

    def checkout(self, email):
        self.mailer.send(email, f"order placed at {self.clock.now()}")


container = Container()
container.register(Clock, lifetime=SINGLETON)
container.register(Database, lifetime=SCOPED)  # one per request
container.register(Mailer, ConsoleMailer)  # ask for the abstraction
container.register(OrderRepository)
container.register(CheckoutService)

for request in (1, 2):
    scope = container.scope()
    a = scope.resolve(CheckoutService)
    b = scope.resolve(CheckoutService)
    print(f"request {request}:")
    a.checkout("ada@example.com")
    print(f"  same service? {a is b}  same db? {a.orders.db is b.orders.db}"
          f"  db id {a.orders.db.id}  same clock? {a.clock is b.clock}")

print("singleton across requests:",
      container.scope().resolve(Clock) is container.scope().resolve(Clock))


class Chicken:
    def __init__(self, egg: "Egg"): ...


class Egg:
    def __init__(self, chicken: Chicken): ...


container.register(Chicken)
container.register(Egg)
for service in (Chicken, dict):
    try:
        container.scope().resolve(service)
    except LookupError as error:
        print("LookupError:", error)
txt
request 1:
  mail to ada@example.com: order placed at 2026-10-20T12:00
  same service? False  same db? True  db id 1  same clock? True
request 2:
  mail to ada@example.com: order placed at 2026-10-20T12:00
  same service? False  same db? True  db id 2  same clock? True
singleton across requests: True
LookupError: cycle: Chicken -> Egg -> Chicken
LookupError: nothing registered for dict

CheckoutService was never constructed by hand. The container read its constructor, saw an OrderRepository, a Mailer and a Clock, built each of those, including the Database the repository needs, and passed them in. It used ConsoleMailer where Mailer was asked for, because that was the registration, and none of the classes know a container exists. The output also shows the three lifetimes at work, and the last two lines show the two errors every container has to report well: a cycle, where two classes each need the other, and a dependency nobody registered.

Lifetimes

The lifetimes are the part of a container that most often goes wrong, so they are worth being precise about. In the terms .NET’s documentation uses:21

LifetimeOne instance perTypical use
TransientEvery request for the serviceLightweight, stateless services
ScopedScope; in a web application, each client requestA database context or unit of work shared by everything handling one request
SingletonThe container’s whole lifetimeConfiguration, caches, clocks; must be safe to use from many threads at once
Service lifetimes in a container, across two requests. Each request gets its own scope. Inside request 1, the checkout service is resolved twice and is a new object each time, because it is transient; both copies share the one database object of that scope, because the database is scoped. Request 2 gets a different database object of its own. The clock is a singleton, so one clock object, owned by the root container, is shared by everything in both requests.root containerClocksingleton: one, everscope: request 1Checkout #1transientCheckout #2transientDatabase Ascopedscope: request 2Checkout #3transientCheckout #4transientDatabase Bscoped
Two requests, two scopes. Transient services are new every time, scoped ones are shared within a request, and the singleton is shared by everything.

The rule that follows from the table is that an object must not hold on to one that is meant to live for less time than it does. A singleton that keeps a scoped database context keeps the first request’s context for every request after it, long after that request has finished. Seemann named this a captive dependency, and .NET’s guidelines list it as an anti-pattern.22

A real container

Production containers do the same job as the sixty-line one, with more care: compiled factories instead of reflection on every call, disposal, open generics, decorators and validation. Here is the same application on .NET’s built-in container, followed by a deliberate captive dependency:

csharp
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddSingleton<IClock, SystemClock>();
services.AddScoped<OrderRepository>();
services.AddTransient<IMailer, ConsoleMailer>();
services.AddTransient<CheckoutService>();

using var provider = services.BuildServiceProvider(validateScopes: true);

for (var request = 1; request <= 2; request++)
{
    using var scope = provider.CreateScope();
    var a = scope.ServiceProvider.GetRequiredService<CheckoutService>();
    var b = scope.ServiceProvider.GetRequiredService<CheckoutService>();
    Console.WriteLine($"request {request}: same service {ReferenceEquals(a, b)}, " +
        $"same repository {ReferenceEquals(a.Orders, b.Orders)} (#{a.Orders.Id}), " +
        $"same clock {ReferenceEquals(a.Clock, b.Clock)}");
}

// A singleton that holds a scoped service would keep one request's
// repository alive for every request after it: a captive dependency.
var bad = new ServiceCollection();
bad.AddScoped<OrderRepository>();
bad.AddSingleton<ReportCache>();
try
{
    bad.BuildServiceProvider(new ServiceProviderOptions
    {
        ValidateScopes = true,
        ValidateOnBuild = true,
    });
}
catch (AggregateException error)
{
    Console.WriteLine(error.InnerExceptions[0].Message);
}

interface IClock { DateTime Now { get; } }
class SystemClock : IClock { public DateTime Now => DateTime.UtcNow; }
interface IMailer { void Send(string to, string text); }
class ConsoleMailer : IMailer
{
    public void Send(string to, string text) => Console.WriteLine($"mail to {to}: {text}");
}
class OrderRepository
{
    static int created;
    public int Id { get; } = ++created;
}
class CheckoutService(OrderRepository orders, IMailer mailer, IClock clock)
{
    public OrderRepository Orders { get; } = orders;
    public IMailer Mailer { get; } = mailer;
    public IClock Clock { get; } = clock;
}
class ReportCache(OrderRepository orders)
{
    public OrderRepository Orders { get; } = orders;
}
txt
request 1: same service False, same repository True (#1), same clock True
request 2: same service False, same repository True (#2), same clock True
Error while validating the service descriptor 'ServiceType: ReportCache Lifetime: Singleton ImplementationType: ReportCache': Cannot consume scoped service 'OrderRepository' from singleton 'ReportCache'.

The lifetimes behave exactly as in the Python version: a new CheckoutService each time, one repository per scope and one clock overall. The second half is the more useful lesson. With scope validation on, which .NET’s default host enables in development, the container refuses to build a provider in which a singleton consumes a scoped service, and says so before a single request is served.14 A good container turns the lifetime rule into an error at start-up rather than a stale-data bug in production.

Service locator: the inversion that isn’t

A service locator also hands out dependencies, and is often confused with dependency injection. The difference is who asks. With a locator, the class asks for what it needs while it runs, usually by calling something like locator.Get<IMailer>() from inside a method; with injection, the class is given what it needs and never asks at all. Fowler noted that both separate a class from the concrete services it uses, and that the locator is simpler, but that it makes every class depend on the locator itself and keeps its dependencies out of sight.10

Seemann went further and called it an anti-pattern: a class that pulls its dependencies from a locator does not show them in its constructor, so neither the compiler nor a reader can tell what it needs, and a missing registration surfaces only when that line of code finally runs.23 .NET’s guidelines give the same advice, to avoid the service locator pattern and calls to GetService where injection can supply the dependency instead.22 A container used as a locator, passed around so that classes can fetch from it, loses most of what injection gains. A container used only in the composition root does not.

IoC, dependency injection and dependency inversion

Three terms with similar names are easy to mix up, and they are not the same thing.

  • Inversion of control is the broad arrangement: someone else’s code decides when yours runs. Callbacks, event loops, template methods and frameworks are all forms of it.
  • Dependency injection is one specific form: someone else decides which objects yours works with, and hands them in. It is how a class gets its dependencies, a mechanism.
  • The dependency inversion principle is a design rule about which way dependencies point. In Martin’s words it has two parts: high-level modules should not depend on low-level modules, both should depend on abstractions; and abstractions should not depend on details, details should depend on abstractions.5 It is the fifth of the SOLID principles, and its own article covers it in full.

The principle and the mechanism meet in practice. In the signup example, SignupService, the high-level policy, depends only on the Mailer abstraction, and the low-level SmtpMailer depends on that abstraction too, by implementing it. That is dependency inversion. Dependency injection is what gets an implementation into the service at run time without the service naming it. Each can exist without the other: a class can receive a concrete SmtpMailer by injection, which is injection without inversion, and a module can depend on an interface it looks up through a locator, which is inversion without injection.

What it costs and what it buys

It buysIt costs
Reuse. A framework’s loop, error handling and plumbing are written once and shared by every program built on itControl. The program no longer decides the order of events, and must learn the framework’s rules for when it will be called
Testability. A class whose collaborators are injected can be tested with stand-ins, as OutboxMailer showedIndirection. Reading the code no longer tells you which implementation runs; the answer is in a registration somewhere else
Loose coupling. Implementations can be swapped, decorated or reconfigured without editing the classes that use themFailures moved later. With a container, a missing or wrong registration is a start-up or run-time error rather than a compile error, unless the container validates the graph
Explicit dependencies. Constructor injection makes everything a class needs visible in one signatureLearning curve and magic. Auto-wiring, lifetimes and scopes are one more system to understand, and a stack trace through a container is long
Managed lifetimes. One place decides what is shared, what is per request and what is cleaned upLifetime bugs. Captive dependencies and shared mutable singletons are new ways to fail

The costs are the reason not to reach for a container by reflex. A command-line tool with ten classes is clearer with a hand-written composition root in main. A web application with hundreds of services, per-request database contexts and a framework that already expects a container is clearer with one.

When to use it

  • Use a framework’s inversion of control whenever the framework fits the problem. Writing your own event loop, HTTP router or test runner is rarely a good use of time, and handing over control is the price of not having to.
  • Inject dependencies through the constructor in any class that talks to something slow, external or nondeterministic, a database, the network, the clock, the file system, so that it can be tested and swapped without editing it.
  • Compose by hand first. Keep the wiring in one composition root near the entry point. Introduce a container when that wiring becomes long, repetitive or needs lifetimes such as per-request scopes, and keep the container in that one place.
  • Do not use a container as a locator. If classes start receiving the container itself, or calling it from inside their methods, the dependencies have gone back into hiding.
  • Turn on validation. If the container can check the graph and its lifetimes at start-up, as .NET’s can, let it.
  • Do not inject what never varies. A pure function, a value object or a standard-library type with no side effects needs no abstraction and no container entry.

Why it matters

Almost every program of any size today runs inside someone else’s main loop: a web framework, a mobile operating system, a browser, a test runner, a cloud function’s runtime. Knowing that this is inversion of control, and that it is a trade, explains why frameworks feel the way they do, why their rules about when code is called matter so much, and why the same idea, applied to where objects come from, turned into dependency injection and the containers that now ship with the platforms. Used with care, it keeps each piece of a program ignorant of the pieces it does not need to know about. Used by reflex, it hides the program’s structure behind registrations nobody can follow. The skill is telling the two apart.

References


  1. Fowler, M. (2005, June 26). InversionOfControl. martinfowler.com. https://martinfowler.com/bliki/InversionOfControl.html ↩︎ ↩︎ ↩︎

  2. Johnson, R. E., & Foote, B. (1988). Designing reusable classes. Journal of Object-Oriented Programming, 1(2), 22–35. http://www.laputan.org/drc/drc.html ↩︎ ↩︎ ↩︎

  3. Sweet, R. E. (1985). The Mesa programming environment. ACM SIGPLAN Notices, 20(7), 216–229. https://doi.org/10.1145/17919.806843 ↩︎ ↩︎

  4. Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. ↩︎ ↩︎

  5. Martin, R. C. (1996). The dependency inversion principle. The C++ Report. https://cs.utexas.edu/~downing/papers/DIP-1996.pdf ↩︎ ↩︎

  6. Apache Excalibur. (n.d.). Avalon Framework: Guide: Inversion of Control. The Apache Software Foundation. https://excalibur.apache.org/framework/guide-patterns-ioc.html; and Introduction, https://excalibur.apache.org/developing/introduction.html ↩︎

  7. Johnson, R. (2006, November 9). Spring Framework: The origins of a project and a name. Spring Blog. https://spring.io/blog/2006/11/09/spring-framework-the-origins-of-a-project-and-a-name/ ↩︎

  8. Risberg, T. (2004, March 24). Spring Framework 1.0 final released. Spring Blog. https://spring.io/blog/2004/03/24/spring-framework-1-0-final-released ↩︎

  9. Hammant, P. (2004, June 5). PicoContainer 1.0 released (finally). https://paulhammant.com/blog/000250 ↩︎

  10. Fowler, M. (2004, January 23). Inversion of control containers and the dependency injection pattern. martinfowler.com. https://martinfowler.com/articles/injection.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  11. Almaer, D. (2007, March 12). Guice, Google’s internal Java dependency injection framework, released as open source. Google Developers Blog. https://developers.googleblog.com/2007/03/guice-google-internal-java-dependency.html ↩︎

  12. Johnson, R., & Lee, B. (2009). JSR 330: Dependency Injection for Java. Java Community Process. https://jcp.org/en/jsr/detail?id=330 ↩︎

  13. Oracle. (n.d.). Annotation Type Inject. Java EE 6 API specification. https://docs.oracle.com/javaee/6/api/javax/inject/Inject.html ↩︎

  14. Microsoft. (2026, January 26). Dependency injection in .NET. Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/overview ↩︎ ↩︎

  15. Angular. (n.d.). Dependency injection in Angular. https://angular.dev/guide/di ↩︎

  16. Angular. (n.d.). Hierarchical injectors. https://angular.dev/guide/di/hierarchical-dependency-injection ↩︎

  17. Spring. (n.d.). Introduction to the Spring IoC container and beans. In Spring Framework Reference Documentation. https://docs.spring.io/spring-framework/reference/core/beans/introduction.html ↩︎

  18. Node.js. (n.d.). The Node.js event loop. https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick ↩︎

  19. Python Software Foundation. (n.d.). unittest: Unit testing framework. Python documentation. https://docs.python.org/3/library/unittest.html ↩︎

  20. Seemann, M. (2011, July 28). Composition Root. ploeh blog. https://blog.ploeh.dk/2011/07/28/CompositionRoot/ ↩︎

  21. Microsoft. (2025, October 21). Service lifetimes. Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes ↩︎

  22. Microsoft. (2026, January 14). Dependency injection guidelines. Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/guidelines ↩︎ ↩︎

  23. Seemann, M. (2010, February 3). Service Locator is an anti-pattern. ploeh blog. https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ ↩︎