Skip to content

Instantly share code, notes, and snippets.

@MangaD
Created August 1, 2026 10:02
Show Gist options
  • Select an option

  • Save MangaD/ab9bce5dced402c216aa98ec9c54c458 to your computer and use it in GitHub Desktop.

Select an option

Save MangaD/ab9bce5dced402c216aa98ec9c54c458 to your computer and use it in GitHub Desktop.
Inversion of Control (IoC): a comprehensive guide

Inversion of Control (IoC): a comprehensive guide

CC0

Disclaimer: ChatGPT generated document.

Inversion of Control (IoC) is a software-design principle in which a component does not control the overall flow of execution or the creation/selection of its dependencies. Instead, some external mechanism—such as a framework, container, runtime, event loop, or orchestrator—controls those things and calls your code at the appropriate time.

The shortest useful description is:

Traditional code: your code calls the library. IoC: the framework/runtime calls your code.

That simple reversal has enormous consequences for architecture, testing, extensibility, dependency management, frameworks, plugins, event-driven systems, and modern application development.


1. The fundamental idea

Consider an ordinary program:

main()
 ├── read configuration
 ├── create database
 ├── create repository
 ├── create service
 ├── call service
 └── print result

Your application explicitly determines:

  • what objects exist,
  • when they are created,
  • how they are connected,
  • what runs next.

For example:

class OrderService {
    private final MySqlOrderRepository repository =
        new MySqlOrderRepository();

    public void placeOrder(Order order) {
        repository.save(order);
    }
}

OrderService controls its dependency.

It decides:

I need a repository
       ↓
I'll use MySqlOrderRepository
       ↓
I'll construct it myself
       ↓
I'll call it myself

That creates strong coupling.

With IoC, some of those decisions move outside the component:

class OrderService {
    private final OrderRepository repository;

    OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public void placeOrder(Order order) {
        repository.save(order);
    }
}

Now:

              external mechanism
                      │
                      │ provides
                      ▼
OrderService ───► OrderRepository

OrderService no longer decides which repository implementation gets created.

That's an inversion of control.


2. What exactly gets "inverted"?

This is where the term can become confusing.

There isn't one single type of control that IoC must invert.

Different architectures invert different forms of control.

Construction control

Instead of:

var repository = new SqlRepository();
var service = new UserService(repository);

a container constructs everything.

Dependency-selection control

Instead of:

PaymentGateway gateway = new StripeGateway();

something external decides which implementation satisfies:

PaymentGateway

Execution-flow control

Instead of your code deciding:

do A
then B
then C

a framework decides when your functions run.

Lifecycle control

The framework may determine when components:

start
initialize
receive work
stop
dispose

Event control

Your program registers handlers:

button.addEventListener("click", handleClick);

The browser decides when handleClick() executes.

Your code doesn't do:

while (true) {
    if (button.wasClicked()) {
        handleClick();
    }
}

The runtime owns the main event loop.

That is IoC too.


3. The "Hollywood Principle"

IoC is closely associated with the phrase:

"Don't call us, we'll call you."

Imagine a framework saying:

Developer:
"When should I call the framework?"

Framework:
"You don't.
Give me your components and callbacks.
I'll call them when they're needed."

This is sometimes called the Hollywood Principle.

It captures control-flow inversion particularly well.


4. Libraries versus frameworks

This distinction provides one of the easiest ways to understand IoC.

Using a library

Your program controls execution:

import math

x = math.sqrt(25)

The application says:

My program
    │
    ├── calls library
    ▼
 Library

Using a framework

The framework often controls execution:

@app.get("/users")
def users():
    return get_users()

You don't normally write:

while True:
    request = wait_for_http_request()

    if request.path == "/users":
        users()

The framework/runtime does something conceptually like that.

 Framework
     │
     │ invokes
     ▼
Your code

That is a classic manifestation of IoC.

A useful heuristic is therefore:

Libraries are generally called by applications; frameworks generally call application code.

It's not an absolute technical definition, but it's an excellent mental model.


5. IoC is NOT the same thing as Dependency Injection

This distinction is extremely important.

People often say:

IoC = Dependency Injection

That's inaccurate.

Dependency Injection (DI) is one technique for implementing IoC.

Think:

                 Inversion of Control
                        │
        ┌───────────────┼─────────────────┐
        │               │                 │
        ▼               ▼                 ▼
 Dependency        Event-driven       Template
 Injection          callbacks          Method
        │
        ▼
 Constructor / Setter / Field injection

IoC is the broader principle.

DI specifically concerns supplying dependencies from outside a component.


6. Dependency Injection

Suppose:

class CheckoutService {

    private StripePaymentGateway gateway =
        new StripePaymentGateway();

}

The class determines its dependency.

We can invert that responsibility:

class CheckoutService {

    private final PaymentGateway gateway;

    CheckoutService(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

Something outside supplies:

new CheckoutService(
    new StripePaymentGateway()
);

This is constructor injection.

The important transformation is:

Before

CheckoutService
      │
      │ constructs
      ▼
StripePaymentGateway

versus:

After

Composition Root
      │
      ├──── constructs ────► StripePaymentGateway
      │
      └──── constructs ────► CheckoutService
                                   ▲
                                   │ receives dependency

The service becomes less concerned with object construction and more concerned with its actual business responsibility.


7. DI without a container

Another common misconception:

Dependency Injection requires a DI framework.

It doesn't.

This is perfectly valid DI:

class UserService:
    def __init__(self, repository):
        self.repository = repository


repository = PostgresUserRepository()
service = UserService(repository)

The programmer manually performs the injection.

This is often called:

manual dependency injection or pure DI.

A DI container merely automates some of the composition.


8. IoC containers

An IoC container is infrastructure that manages components and their dependencies.

Suppose you have:

Controller
    ↓
Service
    ↓
Repository
    ↓
Database

Instead of constructing them manually:

Database db = new Database(config);

Repository repo =
    new SqlRepository(db);

UserService service =
    new UserService(repo);

UserController controller =
    new UserController(service);

a container can maintain something resembling:

UserController
      │
      ▼
UserService
      │
      ▼
UserRepository
      │
      ▼
Database

and construct the object graph automatically.

Containers may handle:

  • dependency resolution
  • component registration
  • object creation
  • configuration
  • scopes
  • lifetimes
  • factories
  • initialization
  • disposal
  • interception
  • proxies
  • decorators
  • lazy initialization

9. Dependency graphs

IoC containers are essentially working with graphs.

Suppose:

OrderController
      │
      ▼
OrderService
   ┌──┴──────┐
   ▼         ▼
Inventory   PaymentService
Repository       │
   │             ▼
   ▼        PaymentGateway
Database

The container determines the construction order.

Conceptually:

Database
    ↓
InventoryRepository

PaymentGateway
    ↓
PaymentService

InventoryRepository + PaymentService
                ↓
           OrderService
                ↓
         OrderController

This becomes valuable as applications grow to hundreds or thousands of components.


10. Major forms of Dependency Injection

There are several common styles.

Constructor injection

class Service {
    Service(Repository repository) {
        this.repository = repository;
    }
}

Usually the preferred approach because dependencies are explicit and can often be immutable.

Setter injection

service.setRepository(repository);

Useful for optional or reconfigurable dependencies, although it allows temporarily invalid objects if used carelessly.

Property/field injection

For example:

@Inject
Repository repository;

Convenient but hides dependencies from the constructor/API and can make isolated testing or reasoning harder.

Method/parameter injection

A dependency is supplied only for a particular operation:

void process(Logger logger) {
}

Useful when the dependency belongs to the operation rather than the object's entire lifetime.


11. IoC without Dependency Injection

IoC occurs in many systems where DI isn't the central mechanism.

Event handlers

button.onclick = () => {
    console.log("clicked");
};

The browser controls when your function runs.

GUI frameworks

You define:

onClick()
onPaint()
onClose()
onKeyPress()

The GUI framework invokes them.

Web frameworks

You define:

@app.get("/hello")
def hello():
    return "Hello"

The framework decides when the handler executes.

Test frameworks

You write:

@Test
void shouldCreateUser() {
}

The test runner discovers and invokes it.

Plugin architectures

A host application loads:

Plugin A
Plugin B
Plugin C

and calls predefined plugin interfaces.

Operating systems

An OS invokes:

  • interrupt handlers
  • signal handlers
  • callbacks
  • event handlers

Game engines

Developers implement hooks resembling:

start()
update()
render()
collision()

The engine controls the main loop.


12. Template Method: an early OO form of IoC

Consider:

abstract class DataProcessor {

    public void process() {
        read();
        transform();
        save();
    }

    protected abstract void transform();
}

A subclass implements:

class CsvProcessor extends DataProcessor {

    protected void transform() {
        ...
    }
}

Notice the direction:

Superclass/framework algorithm
          │
          │ calls
          ▼
Subclass implementation

The parent defines the algorithm while subclasses fill in selected operations.

This is Template Method, and it embodies IoC.


13. History of Inversion of Control

IoC is much older than modern DI frameworks.

Its intellectual roots come from several areas of computing.

Early callback-driven programming

Operating systems and event-driven environments already used inversion-like mechanisms: programs supplied routines that systems invoked when events occurred.

The concept therefore predates the terminology commonly used today.

Framework-oriented programming

Object-oriented frameworks increasingly used patterns where:

framework owns algorithm
application supplies specialization

This became fundamental to GUI frameworks and application frameworks.

1980s–1990s: object-oriented frameworks

Framework architectures made IoC increasingly visible.

Instead of applications controlling every operation, frameworks supplied:

main loop
lifecycle
extension points
callbacks
base classes

Application developers filled in the missing behavior.

1988: Ralph Johnson and Brian Foote

A historically important paper is "Designing Reusable Classes" by Ralph Johnson and Brian Foote.

It discusses reusable object-oriented frameworks and the characteristic reversal where framework code calls application-specific code.

The ideas helped formalize what became known as inversion of control.

Hollywood Principle

The expression:

Don't call us, we'll call you.

became a memorable explanation for framework-based inversion.

1990s: design patterns

The rise of object-oriented design patterns strengthened architectural approaches involving callbacks, factories, strategies, observers, template methods, and abstract interfaces.

The famous 1994 Design Patterns book by Gamma, Helm, Johnson, and Vlissides—the "Gang of Four"—helped popularize many mechanisms closely related to IoC.

Late 1990s / early 2000s: enterprise component containers

Enterprise Java technologies increasingly relied on containers to manage:

transactions
security
persistence
lifecycles
components
configuration

Instead of application code managing infrastructure directly, containers took responsibility.

2000s: lightweight containers

Projects such as Spring popularized a less heavyweight approach based heavily on dependency injection and plain objects.

Inversion of Control Containers and the Dependency Injection pattern

Martin Fowler's influential 2004 article "Inversion of Control Containers and the Dependency Injection pattern" helped establish Dependency Injection as the preferred term for the specific pattern of externally supplying object dependencies.

That terminology was useful because "Inversion of Control" had become too broad:

What control is being inverted?

"Dependency Injection" identifies the specific mechanism.


14. IoC and the Dependency Inversion Principle

These are also commonly confused.

They are related but different.

Inversion of Control

Concerns who controls creation, execution, lifecycle, or orchestration.

Dependency Inversion Principle (DIP)

The D in SOLID.

Roughly:

High-level policy should not depend directly on low-level implementation details; both should depend on abstractions.

Consider:

Bad:

OrderService
     ↓
MySQLRepository

DIP encourages:

OrderService
     ↓
OrderRepository
     ▲
     │ implements
MySQLRepository

DI can then wire them:

Container
   │
   ├── MySQLRepository
   │
   └── inject into OrderService

So:

DIP = architectural dependency direction

DI = mechanism for providing dependencies

IoC = broader transfer/inversion of control

They frequently work together but aren't synonyms.


15. Service Locator versus Dependency Injection

Another approach is:

class OrderService {

    void process() {
        PaymentGateway gateway =
            ServiceLocator.get(PaymentGateway.class);
    }
}

Control has moved away from direct construction, but the service still actively asks for its dependency.

With DI:

class OrderService {

    OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

The dependency comes to the service.

Compare:

Service Locator:

Component ── asks ──► Locator ── returns ──► Dependency

versus:

Dependency Injection:

Container ── provides ──► Component
               +
            Dependency

DI is often preferred because dependencies are visible in the component's API.


16. The Composition Root

A particularly important concept in DI architectures is the Composition Root.

It is the location where the application's object graph is assembled.

For example:

public static void main(String[] args) {

    Database db = new PostgresDatabase();

    UserRepository repository =
        new SqlUserRepository(db);

    EmailSender email =
        new SmtpEmailSender();

    UserService service =
        new UserService(repository, email);

    Application app =
        new Application(service);

    app.run();
}

Notice that most classes don't know how their dependencies are constructed.

Only the composition root does.

               Composition Root
                      │
       ┌──────────────┼──────────────┐
       ▼              ▼              ▼
   Database       Repository       Email
                       │
                       ▼
                    Service
                       │
                       ▼
                  Application

Keeping composition centralized can give you many IoC/DI benefits without using a container at all.


17. IoC in Spring

Spring Framework is perhaps the most famous example of an IoC container in mainstream enterprise development.

A simplified Spring example:

@Repository
class SqlUserRepository
        implements UserRepository {
}

@Service
class UserService {

    private final UserRepository repository;

    UserService(UserRepository repository) {
        this.repository = repository;
    }
}

Spring discovers/manages components and resolves:

UserService requires UserRepository
              ↓
find implementation
              ↓
SqlUserRepository
              ↓
construct dependency
              ↓
construct UserService

Spring calls managed objects beans, and its container handles their creation and lifecycle.


18. IoC in .NET

Microsoft .NET has dependency injection deeply integrated into its modern application stack.

Conceptually:

services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<UserService>();

Then:

public class UserService
{
    private readonly IUserRepository repository;

    public UserService(IUserRepository repository)
    {
        this.repository = repository;
    }
}

The runtime/framework resolves the graph.


19. IoC in JavaScript and frontend frameworks

IoC appears even without traditional DI containers.

Consider a UI framework:

function Button() {
    return <button onClick={handleClick}>Click</button>;
}

The application doesn't directly control:

DOM event loop
render scheduling
event dispatch
component lifecycle

The framework/browser controls much of this.

Likewise hooks such as:

useEffect(() => {
    ...
}, []);

register behavior that the framework decides when to execute.

So frontend frameworks are full of control inversion even where developers rarely use the phrase "IoC."


20. IoC in Node.js

Consider:

server.on("request", (req, res) => {
    ...
});

Your application registers a callback.

The runtime controls:

wait for network event
      ↓
receive request
      ↓
invoke callback

Again:

Don't call us; we'll call you.


21. IoC in Python

Frameworks such as web servers and testing systems demonstrate the same principle.

For example:

def test_addition():
    assert 1 + 1 == 2

You don't necessarily write:

test_addition()

The test runner discovers and invokes it.

Likewise web frameworks invoke route handlers.


22. IoC in Android and mobile development

Mobile platforms heavily invert control.

Developers provide lifecycle hooks resembling:

onCreate()
onStart()
onResume()
onPause()
onStop()
onDestroy()

Your application doesn't decide when the operating system pauses your activity.

The platform does.

Android/system lifecycle
          │
          ▼
       onCreate
          │
          ▼
       onStart
          │
          ▼
       onResume

The framework owns lifecycle control.


23. IoC in game development

Game engines often expose functions resembling:

void Start() {
}

void Update() {
}

The engine runs:

while game_running:

    process_input()

    update_physics()

    call_user_Update()

    render()

Developers provide behavior; the engine owns the execution loop.

This is a textbook IoC architecture.


24. IoC in testing

Testing frameworks provide another clean example.

You write:

@BeforeEach
void setup() {}

@Test
void createsUser() {}

@AfterEach
void cleanup() {}

The test framework determines:

discover tests
      ↓
instantiate test class
      ↓
setup()
      ↓
test()
      ↓
cleanup()

Your code supplies extension points inside the framework's execution algorithm.


25. IoC in plugins

Suppose an editor supports extensions.

The host defines:

interface Plugin {
    void initialize();
    void execute();
}

Plugins implement:

class MyPlugin implements Plugin {
    ...
}

The application performs:

discover plugins
      ↓
load plugins
      ↓
initialize plugins
      ↓
invoke plugin functionality

Plugins don't control the host.

The host controls plugins.

This architecture enables applications such as IDEs, browsers, editors, build systems, and game engines to be extensible.


26. IoC in event-driven architecture

Consider a message consumer:

@MessageListener("orders")
void handle(OrderCreated event) {
    ...
}

The application doesn't necessarily decide:

now call handle()

Instead:

message broker
      ↓
runtime
      ↓
dispatcher
      ↓
handler

Event-driven architecture therefore frequently contains IoC at multiple levels.


27. Push versus pull

A useful conceptual distinction is:

Pull model

Your code asks:

Do you have work?
Do you have work?
Do you have work?

Example:

while True:
    message = queue.get()
    process(message)

Push/inverted model

You register:

on_message(process)

Then:

runtime receives message
        ↓
runtime invokes process()

The latter demonstrates execution control being moved outward.


28. Object lifecycle management

IoC containers frequently manage object lifetimes.

Typical scopes include:

Singleton
    One instance for application/container

Transient
    New instance each resolution

Request
    One instance per HTTP request

Session
    One instance per user/session

Thread
    One instance per thread

Example:

HTTP Request #1
 ├── Controller A
 ├── Service A
 └── RequestContext #1

HTTP Request #2
 ├── Controller B
 ├── Service B
 └── RequestContext #2

The application components don't need to manually create and destroy each request-scoped object.


29. Configuration and IoC

IoC often externalizes configuration as well.

Instead of:

Database db =
    new Database(
        "localhost",
        5432,
        "production"
    );

the component receives configuration:

Database(DatabaseConfig config)

and the environment/container supplies:

environment variables
configuration files
secret manager
command-line options
         │
         ▼
     configuration
         │
         ▼
      container
         │
         ▼
      Database

This separates configuration from behavior.


30. Factories and IoC

Factories can participate in IoC.

Instead of:

new PdfExporter()

you might use:

Exporter exporter =
    exporterFactory.create(format);

The component delegates construction decisions.

A container may itself rely heavily on factories internally.


31. Strategy pattern and IoC

Suppose:

class PriceCalculator {

    private PricingStrategy strategy;

    PriceCalculator(PricingStrategy strategy) {
        this.strategy = strategy;
    }
}

Different strategies:

RegularPricing
HolidayPricing
PremiumPricing

External code selects the strategy.

        configuration
             │
             ▼
      PricingStrategy
             │
             ▼
      PriceCalculator

The calculator doesn't determine which algorithm it uses.

That represents dependency-level control inversion.


32. Observer pattern and IoC

Observer/event systems naturally invert execution control:

Publisher
    │
    ├── notify Observer A
    ├── notify Observer B
    └── notify Observer C

Observers register behavior and wait to be called.

This idea appears everywhere:

DOM events
message buses
GUI systems
reactive programming
domain events
webhooks
event emitters

33. IoC and callbacks

The simplest form of IoC may simply be passing a function:

fetchData(result => {
    console.log(result);
});

You provide:

what should happen

while something else controls:

when it happens

That separation is fundamental to asynchronous programming.


34. IoC and higher-order functions

Consider:

items.map(transform)

You provide transform.

The map implementation determines:

how iteration occurs
when transform runs
how results are collected

At a small scale, this contains the same structural idea:

algorithm owns control
      │
      ▼
calls supplied behavior

IoC therefore isn't restricted to giant enterprise frameworks.


35. IoC and middleware

Web frameworks often use middleware:

Request
   ↓
Authentication
   ↓
Logging
   ↓
Authorization
   ↓
Controller
   ↓
Response

You implement middleware components.

The framework determines:

when they execute
what request reaches them
how errors propagate
how the chain is assembled

Again, control resides primarily in infrastructure.


36. IoC and aspect-oriented behavior

Containers can intercept operations:

Controller
    ↓
Service proxy
    ↓
┌───────────────┐
│ transaction   │
│ logging       │
│ authorization │
│ metrics       │
└───────────────┘
    ↓
actual service

The service might contain:

void transferMoney(...) {
    ...
}

while infrastructure handles:

start transaction
call service
commit transaction

or:

start transaction
call service
exception
rollback

This removes certain cross-cutting concerns from business code.


37. Why use IoC?

The main benefits come from separating policy/behavior from orchestration and infrastructure.

A well-designed component can say:

"Here's what I need and what I do."

instead of:

"Here's what I need, how to construct it, where to find it, how to configure it, when to start it, and how to dispose of it."

That separation can dramatically improve architecture.


38. Benefit: lower coupling

Without IoC:

class ReportService {

    MySqlDatabase database =
        new MySqlDatabase();

    SmtpMailer mailer =
        new SmtpMailer();
}

ReportService knows specific technologies.

With abstractions:

class ReportService {

    ReportService(
        Database database,
        Mailer mailer
    ) {}
}

It only knows the capabilities it requires.


39. Benefit: testability

Consider:

class CheckoutService {

    CheckoutService(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

Production:

CheckoutService
      ↓
StripeGateway

Test:

CheckoutService
      ↓
FakePaymentGateway

Test:

var fakeGateway =
    new FakePaymentGateway();

var service =
    new CheckoutService(fakeGateway);

No real payment API is required.

This is one of DI's most practical advantages.


40. Benefit: replaceability

Suppose your application depends on:

interface Storage {
    void save(File file);
}

Possible implementations:

LocalStorage
S3Storage
AzureBlobStorage
MemoryStorage

Business logic depends on:

Storage

rather than any particular vendor.

Composition determines the actual implementation.


41. Benefit: centralized lifecycle management

Containers can manage expensive resources:

database pools
HTTP clients
thread pools
caches
message-broker connections

Rather than each component constructing its own resources.


42. Benefit: separation of concerns

A service should ideally concentrate on business behavior:

OrderService:

✓ validate order
✓ calculate order
✓ submit order

not necessarily:

✗ parse config
✗ create DB pool
✗ configure HTTP client
✗ discover logger
✗ initialize metrics

IoC helps move infrastructure responsibilities toward the application's edges.


43. Benefit: extensibility

Frameworks can define stable extension points:

Framework
    │
    ├── AuthenticationProvider
    ├── StorageProvider
    ├── Renderer
    └── Plugin

Applications supply implementations.

The framework doesn't need to know every future implementation.


44. But IoC has costs

IoC isn't automatically good.

It introduces architectural trade-offs.

A major one is indirection.

Compare:

var service =
    new UserService(
        new SqlRepository()
    );

with a large container-managed system where you see:

UserService service;

and wonder:

Where did this object come from?

The answer might involve:

annotations
component scanning
configuration
container registration
profiles
factories
proxies
conditional bindings

Understanding runtime behavior can become harder.


45. "Magic"

Poorly designed IoC systems can feel magical:

I declared an interface.

Somehow an object appeared.

Which implementation?

Why?

When was it created?

Is it singleton?

Why is it proxied?

Why did this annotation change it?

The more implicit the wiring, the greater the cognitive cost.

This is why many developers favor explicit constructor injection even when using a container.


46. Runtime errors

Manual construction may produce compile-time-visible problems.

A dynamically configured container may fail during startup:

Cannot resolve dependency:
PaymentGateway required by CheckoutService

or worse, during runtime.

Modern frameworks mitigate this through static analysis, generated wiring, startup validation, and compile-time DI.


47. Hidden dependency problems

Field injection:

@Inject
Logger logger;

@Inject
Repository repository;

@Inject
PaymentGateway payment;

can hide the fact that the class requires three collaborators.

Constructor injection makes that explicit:

CheckoutService(
    Logger logger,
    Repository repository,
    PaymentGateway payment
)

If the constructor grows to:

Service(
    A a,
    B b,
    C c,
    D d,
    E e,
    F f,
    G g,
    H h
)

that's valuable architectural feedback.

The class may have too many responsibilities.


48. Over-abstraction

A notorious IoC/DI mistake is creating interfaces for everything:

IUserService
UserService

IUserRepository
UserRepository

ILoggerService
LoggerService

IClockService
ClockService

An abstraction is useful when it represents a meaningful boundary, variation point, test seam, or architectural contract.

Creating:

interface Foo {
}

solely because a DI container exists usually adds ceremony rather than flexibility.


49. Dependency Injection doesn't automatically mean loose coupling

You can inject concrete classes:

Service(MySqlRepository repository)

That's still DI.

Conversely, using an interface doesn't automatically produce good architecture.

Good decoupling depends on where abstractions are placed and what architectural decisions they isolate.


50. Dependency Injection versus mocking

DI is sometimes explained as:

"It's for mocking."

That's too narrow.

Testing is one benefit.

DI's more fundamental purpose is:

Separating object use from object construction/composition.

That enables:

testing
configuration
implementation substitution
lifecycle management
modularity
deployment variation

Mocking is only one application.


51. Control inversion at different architectural levels

It's useful to think of IoC as a spectrum.

Function level
    callback

Object level
    injected dependency

Algorithm level
    template method

Application level
    framework

Runtime level
    event loop

System level
    orchestrator/platform

The same underlying idea repeats at different scales.


52. Framework inversion

A traditional standalone program:

main()
 │
 ├── call A
 ├── call B
 ├── call C
 └── exit

A framework application:

Framework main loop
      │
      ├── call application hook
      │
      ├── call application handler
      │
      ├── call application callback
      │
      └── repeat

The application becomes a participant inside a larger execution environment.


53. Declarative programming and IoC

IoC often accompanies declarative systems.

Instead of:

Create server.
Open socket.
Listen.
Parse request.
Route request.
Serialize response.

you declare:

@app.get("/users")
def users():
    ...

You describe:

"When a GET request arrives here, use this."

The framework handles the procedural machinery.

Similarly:

HTML
CSS
SQL
Kubernetes manifests
build configurations
CI workflows

can involve declarative relationships where another system controls execution.

Not every declarative system should automatically be labeled IoC, but the concepts frequently overlap.


54. IoC and Clean/Hexagonal Architecture

IoC becomes particularly important at architectural boundaries.

Suppose:

              Application Core

              OrderService
                   │
                   ▼
             PaymentPort
                   ▲
                   │
        ┌──────────┴──────────┐
        │                     │
 StripeAdapter          FakeAdapter

The business layer defines what capability it needs.

Infrastructure supplies the implementation.

At runtime:

infrastructure → injects → application

while source-code dependency can point toward abstractions owned by the application.

This is central to architectures described as:

  • Hexagonal Architecture
  • Ports and Adapters
  • Clean Architecture
  • Onion Architecture

55. IoC in microservices

Inside a microservice:

HTTP Controller
       ↓
Application Service
       ↓
Domain
       ↓
Ports
       ↑
       │
Adapters

IoC is commonly used to wire:

database adapters
message publishers
external APIs
caches
authentication
metrics

For example:

PaymentService
      ↓
PaymentGateway interface
      ↑
      │
StripeAdapter

The application core doesn't need to know how the vendor SDK is constructed.


56. IoC and serverless systems

Serverless computing is an extreme example of lifecycle inversion.

You write:

exports.handler = async event => {
    ...
};

You don't control:

process startup
server provisioning
request routing
instance scheduling
shutdown

The platform invokes your handler.

Conceptually:

Cloud platform
      ↓
event
      ↓
your function
      ↓
result

That's very much an IoC-style execution model.


57. IoC and Kubernetes

At a broader systems level, orchestration can resemble control inversion.

You declare desired state:

I want 3 instances of this service.

An orchestrator determines:

where they run
when they restart
how replacement occurs

This isn't the classic object-oriented definition of IoC, so calling Kubernetes an "IoC container" would be misleading.

But the broader architectural idea—declaring behavior/state while an external system owns orchestration—is strongly analogous.


58. IoC and reactive programming

Reactive systems frequently reverse traditional control.

Instead of:

ask repeatedly for new data

you subscribe:

stream.subscribe(value => {
    ...
});

Then:

source emits
     ↓
runtime propagates
     ↓
subscriber invoked

The stream controls when data reaches your component.


59. IoC and inversion of communication

A useful generalization is:

Traditional:

Consumer → Producer
"Give me something now."

versus:

Inverted:

Consumer → registers interest
Producer → later invokes consumer

This pattern appears in:

callbacks
events
observers
subscriptions
reactive streams
message consumers
webhooks

60. IoC versus event-driven architecture

Event-driven architecture often uses IoC, but they're not synonymous.

Event-driven architecture describes systems organized around events.

IoC describes control being delegated/inverted.

You can have IoC without events:

constructor injection
template methods

and event-driven architectures usually contain substantial IoC.


61. IoC versus callbacks

A callback is a mechanism.

IoC is the design relationship.

For example:

setTimeout(doSomething, 1000);

The callback is:

doSomething

The inversion is:

timer/runtime decides when doSomething executes

62. IoC versus Dependency Inversion Principle

The cleanest cheat sheet is:

Concept Main question
IoC Who controls execution/composition?
DI Who supplies dependencies?
DIP What direction should source dependencies point?
Service Locator Where does a component obtain dependencies?
Factory Who constructs an object?
Callback Who invokes supplied behavior later?

63. Practical example

Imagine an online store.

Without IoC:

class CheckoutService {

    private MySqlOrderRepository repo =
        new MySqlOrderRepository();

    private StripeGateway stripe =
        new StripeGateway();

    private SmtpEmailSender email =
        new SmtpEmailSender();

    void checkout(Order order) {

        stripe.charge(order.total());

        repo.save(order);

        email.sendReceipt(order);
    }
}

The class is responsible for both:

business logic
+
infrastructure selection/construction

Now introduce boundaries:

class CheckoutService {

    private final OrderRepository repo;
    private final PaymentGateway payment;
    private final EmailSender email;

    CheckoutService(
        OrderRepository repo,
        PaymentGateway payment,
        EmailSender email
    ) {
        this.repo = repo;
        this.payment = payment;
        this.email = email;
    }

    void checkout(Order order) {

        payment.charge(order.total());

        repo.save(order);

        email.sendReceipt(order);
    }
}

Production wiring:

CheckoutService
   │
   ├── SqlOrderRepository
   ├── StripePaymentGateway
   └── SmtpEmailSender

Tests:

CheckoutService
   │
   ├── InMemoryOrderRepository
   ├── FakePaymentGateway
   └── FakeEmailSender

The business class doesn't change.

That is the practical power of dependency-level IoC.


64. When should you use IoC?

IoC/DI becomes particularly useful when:

  • components have meaningful external dependencies;
  • implementations vary by environment;
  • infrastructure should be separated from domain logic;
  • components need isolated tests;
  • object lifetimes require management;
  • plugins or extension points are needed;
  • a framework controls application lifecycle;
  • cross-cutting infrastructure needs centralized management;
  • the dependency graph is large enough that centralized composition helps.

65. When you probably don't need an IoC container

Consider a tiny program:

parser = Parser()
formatter = Formatter()
app = App(parser, formatter)

app.run()

Adding:

container registrations
reflection
annotations
automatic scanning
lifecycle configuration

may make it worse.

IoC is a principle.

An IoC container is optional infrastructure.

Manual DI can often be simpler.


66. A useful design progression

Many applications can evolve like this:

Stage 1

class A:
    b = B()

Then:

Stage 2

class A:
    def __init__(self, b):
        self.b = b

Then composition:

Stage 3

b = B()
a = A(b)

Only when complexity justifies it:

Stage 4

container.register(B)
container.register(A)

a = container.resolve(A)

This avoids introducing container complexity before it's actually useful.


67. Common IoC anti-patterns

Several problems show up repeatedly.

Service Locator everywhere: classes pull arbitrary dependencies from a global container, making their true requirements invisible.

Container leaking into domain code: business logic starts depending on container APIs.

Excessive interfaces: every class gets an interface despite there being no useful abstraction.

Field injection everywhere: dependencies become hidden and objects can exist in partially initialized states.

Huge dependency graphs: DI makes architectural coupling easier to see, but doesn't automatically fix it.

Automatic scanning everywhere: discovering where a dependency came from becomes difficult.

Container as global state: the IoC container becomes a sophisticated global-variable registry.

Mocking everything: tests reproduce implementation structure rather than testing meaningful behavior.


68. Best practices

For dependency-oriented IoC, a strong default is:

1. Prefer constructor injection.

2. Keep dependencies explicit.

3. Keep the container near the application boundary.

4. Keep domain/business code unaware of the container.

5. Use meaningful abstractions, not interfaces mechanically.

6. Prefer manual composition when the graph is simple.

7. Validate the dependency graph at startup.

8. Understand component lifetimes.

9. Avoid injecting enormous numbers of dependencies.

10. Treat IoC as architecture—not an annotation trick.

69. The deepest conceptual interpretation

IoC is ultimately about separating two questions:

What should happen?

from:

Who decides when/how it happens?

For example:

Business component:

"If asked to process an order,
here is how order processing works."

Infrastructure:

"I'll determine when you're instantiated,
what implementations you receive,
when requests reach you,
and when you're destroyed."

That division allows business logic to become more independent of its execution environment.


70. IoC as "policy versus mechanism"

Another useful perspective is:

Policy
"What should the application do?"

        separated from

Mechanism
"How is execution/resources/infrastructure managed?"

A framework or container often manages mechanisms while application code supplies policy.

That idea goes well beyond object construction.


71. The key mental model

Imagine two programmers.

Without IoC

Your component says:

I need a database.

I'll construct PostgreSQL.

I need payments.

I'll construct Stripe.

I need email.

I'll configure SMTP.

Now I'll perform my business logic.

With IoC

Your component says:

To do my job I require:

- an OrderRepository
- a PaymentGateway
- an EmailSender

Give me implementations and I'll perform checkout.

Another part of the system answers:

For production:

OrderRepository → PostgreSQL
PaymentGateway  → Stripe
EmailSender     → SMTP

For tests:

OrderRepository → Memory
PaymentGateway  → Fake
EmailSender     → Fake

The component knows what capabilities it requires, but not necessarily how those capabilities are constructed or selected.

That's the essence.


72. One diagram tying everything together

                    INVERSION OF CONTROL
                            │
           ┌────────────────┼────────────────┐
           │                │                │
           ▼                ▼                ▼
      Construction       Execution        Lifecycle
        Control           Control          Control
           │                │                │
           ▼                ▼                ▼
     Dependency         Callbacks       Framework/
      Injection          Events          Container
           │                │
    ┌──────┼──────┐         ├── GUI
    ▼      ▼      ▼         ├── Web
Constructor Setter Field    ├── Tests
                            ├── Games
                            └── Reactive

And related—but distinct—ideas surround it:

              IoC
               │
       ┌───────┴────────┐
       │                │
       ▼                ▼
      DI             Callbacks
       │
       ▼
Composition
       │
       ├──── may use ────► IoC Container
       │
       └──── may be ─────► Manual

Related architectural principle:

      Dependency Inversion Principle
                   │
                   ▼
        depend on abstractions

The definition worth remembering

If you remember only one thing, make it this:

Inversion of Control means moving control over some aspect of a program—such as execution, dependency creation, dependency selection, or lifecycle—from the component itself to an external mechanism.

And if you remember three distinctions:

IoC = the broad principle.

DI = externally supplying a component's dependencies.

IoC container = infrastructure that can automate dependency composition and lifecycle.

The reason IoC matters isn't that new is inherently bad or that every class needs an interface. Its value is separating components from decisions they shouldn't need to own. At its best, your business code describes its capabilities and requirements, while frameworks, composition roots, runtimes, and infrastructure handle the machinery around it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment