Skip to content

IoC and DI: Sorting Out Their Relationship

You need to know why we use frameworks if you want to make a living.

Makonea·Apr 21, 2026·28 min

Object-oriented programming is as much a different way of designing programs as it is a different way of designing programming languages. This paper describes what it is like to design systems in Smalltalk. In particular, since a major motivation for object-oriented programming is software reuse, this paper describes how classes are developed so that they will be reusable.

What is IoC?

Inversion of Control refers to a software design pattern in which a program written by a programmer receives flow control from a reusable library.

It is called Inversion of Control because, in traditional programming, the flow is driven by the programmer's own code calling out to external library code.

In a structure where Inversion of Control is applied, however, the external library's code calls the programmer's code.

The origins of IoC

"Designing Reusable Classes," written by Ralph E. Johnson and Brian Foote in 1988, is

a landmark paper that systematically addresses reusability in OOP.

In this paper, the two authors define the principles of OOP as follows.

Polymorphism: the property that allows diverse objects to interact through the same message

Protocol: a clear definition of the set of services or methods that objects must provide, i.e., the interface

Inheritance: a mechanism by which a new class inherits the characteristics and behaviors of an existing class, and can extend or modify them

Abstract Classes: classes that define an interface or basic structure that subclasses must implement, without providing a concrete implementation

These definitions differ somewhat from the OOP terminology we use today. This is because OOP at the time was

in a transitional period, moving from Alan Kay's message-centric concept toward the modern concept established by Booch and others.

이행하는 과도기였기 때문이다.

This inversion of control gives frameworks

the power to serve as extensible skeletons

This paper is considered one of the works that established the concept of the modern framework, and it describes IoC as follows: unlike a library that application code calls directly, a framework itself operates by calling methods defined by the user.

The paper names this "inversion of control" and explains that it is the key to making a framework an extensible skeleton.

Framework = Component + Inversion of Control

This paper is regarded as one of the works that established the concept of the modern framework, describing IoC as a way for the framework to

control the flow of an application. Unlike traditional programs in which the developer directly

controls the flow, an external entity (the framework) controls the flow, and the developer only

implements the parts that are needed.

It should be noted that this paper did not originate the concept of IoC from scratch. Similar ideas had already been

circulating sporadically before it, but Designing Reusable Classes (1988) is credited as the paper that

systematically organized and established the terminology.

Let's look at a simple example.

In the traditional programming approach, an object creates itself when needed, or creates and uses other objects.

In other words, the object itself drives the flow of the program.

// Traditional approach (structure where objects are created directly inside the class)
class B {

    // A is directly instantiated and used inside class B
    public void doSomethingWithA() {
        A a = new A(); // B directly creates the A object (tight coupling)
        a.action();    // Calls a method on the created A object
    }
}

class A {

    // Functional method of class A
    public void action() {
        System.out.println("A의 액션"); // Prints a message to the console
    }
}

// Test code
public class Main {

    public static void main(String[] args) {

        B b = new B();        // Creates a B object
        b.doSomethingWithA(); // B internally creates A and executes `action()`
    }
}

With IoC applied, on the other hand, an object does not directly create or look up the objects it needs.

Instead, an external container creates the required objects, configures the dependencies between them, and provides them.

This allows each object to focus solely on its core logic.

The example that most concisely illustrates this is

Constructor Injection, one of the most fundamental forms of IoC.

// Class A (Dependency Provider)
class A {

    // Functional method of Class A
    public void action() {
        System.out.println("A의 동작");
    }
}

// Class B (Dependency Consumer)
// Structure that receives A injected via the Constructor
class B {

    private A a; // Dependency object (reference to A)

    // Constructor Injection
    // Receives the A object from outside and stores it in an internal field
    public B(A a) {
        this.a = a;
    }

    // Method that uses the injected A object
    public void doSomethingWithA() {
        a.action(); // Calls the method of the received A instance
    }
}

// Class acting as an external container
public class Main {

    public static void main(String[] args) {

        A a = new A();   // Creates the A object externally (acting as a container)
        B b = new B(a);  // Injects A by passing it when creating the B object (Dependency Injection)

        b.doSomethingWithA(); // B uses the injected A to execute the method
    }
}

Admittedly, leaving an example on its own like this can make it hard to understand.

So let me introduce the core concepts of IoC.

Don't Call, We'll Call You - Sugarloaf 1974

The general core concepts of IoC can be summarized as follows.

The Mesa Programming Environment — Richard E. Sweet, 1985

Hollywood Principle:

The name "Hollywood Principle" derives from Hollywood film studios' audition practices, though the phrase itself first appeared in Sugarloaf's 1974 song "Don't Call Us, We'll Call You." It first appeared in software literature in a 1983 paper describing the Mesa programming environment, was later written about by John Vlissides, one of the Gang of Four widely regarded as fathers of modern OOP, in a C++ Report article, and became widely known after Martin Fowler cited it.

This principle is drawn from the idea of a Hollywood studio telling an auditioning actor not to call them, but to leave contact information so the studio can call the actor when needed.

Put simply:

1. Rather than an actor (caller) proactively contacting a director (framework) to beg for a role, the director selects the actor they need and makes the "callback" call themselves. This analogy emphasizes that control rests with the director, not the actor.

In any case, IoC means inversion of control, referring to the transfer of flow control from the developer to the framework. The developer writes code following the rules and APIs provided by the framework, while the framework manages the lifecycle and interactions of objects.

Looking at the most commonly seen IoC implementation patterns, they can broadly be grouped into three categories.

The patterns below are IoC patterns that invert "flow control" at the code level or through interactions between objects.

Callback

Commonly seen in event handling, GUI frameworks, and similar contexts.

Template Method Pattern

A pattern in which the parent class defines the overall algorithm flow (the template), and subclasses override specific steps.

Publisher-Subscriber Pattern

When a Subject triggers a specific event, it sends a notification to registered Subscribers.

(Diagram of Spring's IoC Container)

The pattern below delegates object creation and dependency wiring (injection) to a framework or container, so that users need only define the "required dependencies" and have them injected automatically.

IoC Container

The IoC container is what actually implements and manages IoC. It fulfills the following roles.

1. Object creation and management: creates the objects required by the application and manages their lifecycle (creation, initialization, destruction, etc.).

2. Dependency Injection (DI): analyzes the dependency relationships between objects and injects the required objects into other objects.

This allows objects to reduce coupling with one another and operate independently.

3. Object lookup and provision: retrieves objects registered in the container and provides them as needed.

The advantages of IoC are as follows.

Reduced coupling: decreases dependencies between objects and improves code reusability.

Improved flexibility: dependencies can be changed easily through configuration files or other mechanisms.

Simplified code: less boilerplate for object creation and dependency management, making the code more concise.

Readers who have been following along may have been surprised to suddenly see the term Dependency Injection (DI) appear in the middle of a discussion about IoC.

Let me now explain what DI is.

First, doesn't the concept of IoC cover such a broad range that it becomes difficult to understand?

That is why Martin Fowler, the great sage and senior programmer among us, wrote an article after extensive discussions with various people.

That article is none other than the celebrated piece destined to go down in programming history:

Inversion of Control Containers and the Dependency Injection pattern (2004).

In this essay, he organizes the various forms of IoC and defines the concept of Dependency Injection (DI) as one specific pattern within IoC.

제어의 역전이라는 용어는 너무 일반적이어서 사람들이 혼란스러워합니다.
그렇기때문에 다양한 IoC 옹호자들과 많은 논의를 거쳐 의존성 주입이라는 이름을 사용하기로 결정했습니다.

Here are three notable forms of DI as examples.

The three common forms of DI

// ============================
// 1. Constructor Injection
// ============================

// Class A (Dependency Provider)
class A {
    public void action() {
        System.out.println("A is performing its operation."); // Actual logic performed by object A
    }
}

// Class B (Dependency Consumer)
// Receives A via the constructor.
class B {

    // Dependency that B requires
    private A a;

    // Receives A from outside via the constructor.
    public B(A a) {
        this.a = a; // Stores the received A object in an internal field
    }

    // Method that uses A's functionality
    public void doSomethingWithA() {
        a.action(); // Calls a method on the injected A object
    }
}

// External container (responsible for creating and wiring dependencies)
public class Main {

    public static void main(String[] args) {

        // Container creates the A object
        A a = new A();

        // Container creates the B object while injecting A into it
        B b = new B(a);

        // Executes B's functionality
        b.doSomethingWithA();
    }
}

Constructor Injection:

dependencies are injected through the constructor at object creation time. (Widely used in Spring and DI containers.)

// ============================
// 2. Setter Injection
// ============================

// Class A (dependency provider)
class A {

    public void action() {
        System.out.println("A is performing its operation."); // Performs the actual operation
    }
}

// Class B (dependency consumer)
// Receives the dependency via a setter method.
class B {

    // The dependency that B requires
    private A a;

    // Injects A from outside via a setter method
    public void setA(A a) {
        this.a = a;
    }

    // Method that uses A's functionality
    public void doSomethingWithA() {

        // Checks whether the dependency has been injected
        if (a != null) {

            // Uses the injected A object
            a.action();

        } else {

            // When the dependency has not been injected
            System.out.println("A has not been injected.");
        }
    }
}

// External container
public class Main {

    public static void main(String[] args) {

        // Container creates the A object
        A a = new A();

        // Container creates the B object
        B b = new B();

        // Injects A via the setter
        b.setA(a);

        // Executes B's functionality
        b.doSomethingWithA();
    }
}

Setter Injection:

dependencies are injected through setter methods. (Allows various configurations; commonly used with XML, etc.)

// ============================
// 3. Interface Injection
// ============================

// Interface for dependency injection
// A contract that specifies a given object is capable of receiving an injection of A
interface InjectableA {

    // Method that receives the injection of A
    void injectA(A a);
}

// Class A (dependency provider)
class A {

    public void action() {
        System.out.println("A is performing its operation."); // Performs the actual logic
    }
}

// Class B (dependency consumer)
// Implements the `InjectableA` interface to receive the injection of A.
class B implements InjectableA {

    // The dependency that B requires
    private A a;

    // Implements the injection method defined in the interface
    @Override
    public void injectA(A a) {
        this.a = a; // Stores the received A in an internal field
    }

    // Method that uses the functionality of A
    public void doSomethingWithA() {

        // Checks whether the dependency has been injected
        if (a != null) {

            // Uses the injected A
            a.action();

        } else {

            // When the dependency has not been injected
            System.out.println("A has not been injected.");
        }
    }
}

// External container
public class Main {

    public static void main(String[] args) {

        // Container creates an instance of A
        A a = new A();

        // Container creates an instance of B
        B b = new B();

        // Injects A via the interface method
        b.injectA(a);

        // Executes the functionality of B
        b.doSomethingWithA();
    }
}

Interface Injection:

dependencies are injected through an interface. (Used frequently by the author, primarily when a strong contract between specific modules is required.)

Injection method

Advantages

Disadvantages

When to use

Constructor Injection

Dependencies are injected at object creation time, guaranteeing immutability; the object is created in a fully initialized state.

Difficult to handle circular dependencies.

When the dependency is absolutely required.

Setter Injection

Supports optional dependencies; circular dependencies are relatively easier to resolve.

Dependencies can change after object creation, making immutability difficult to guarantee; the object's state may change.

When the dependency is not mandatory.

Interface Injection

Forces only specific objects to be injectable through a particular interface.

Implementation is cumbersome and increases code complexity; rarely used in real-world frameworks.

When a specific injection pattern must be enforced.

Finally, let's summarize this entire post in just two lines.

IoC (Inversion of Control): a higher-level concept that delegates control flow to an external entity. Can be implemented in various ways, including frameworks, callbacks, and events.

DI (Dependency Injection): a lower-level concept that implements IoC. A specific pattern in which dependencies between objects are injected from the outside.

Finally, IoC and DI are not merely technical tools; they reflect a philosophy of software design.

Through them, we can reduce coupling between objects and improve both the flexibility and testability of our code.

Understanding and applying these principles will be of great help in designing and developing better software.