---
title: 'Inversion of Control'
source: 'https://academia.sh/en/courses/enterprise-application-framework/inversion-of-control'
course: 'The Enterprise Application Framework'
language: en
updated: '2026-10-21T05:03:16+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes, using reflection, which expression constructs an object in the source from the case where the same instance returns without construction'
    - 'Runs an instance built by the object container side by side with one built manually with new and shows the difference'
    - 'Proves by running it that an object left outside the container receives none of the annotations at all'
---

# Inversion of Control

An IoC container derives an object's construction, the return of the same instance, and a manually new-built instance receiving none of the annotations at all from three separate sources: of twelve behaviors, three come from a call in the source, six from an annotation alone, three from the container's own default.

The closing lesson of the Build, Testing and Ecosystem course measured that the source text does
not say where a class enters the runtime class path from: the calling side only saw an
interface, it never mattered whether the class behind it had passed through a Java compiler.
This lesson carries the same gap one step further. There, what was missing was *where* a type
came from; here, what is missing is *who* constructs an object. In a program that uses an
application framework, someone searching for the `new` keyword most often finds nothing at
all — the objects exist, they work, but the line that constructs them is not in the source.

The mechanism that fills this gap is **inversion of control**: not the program itself, but an
**IoC container** running the program decides when an object is constructed, with which
dependencies, and how many times. Normally, the code that writes a program's flow determines it —
the writing side decides which class is instantiated and in what order it is called. When the
container takes over this decision, control changes direction: the party deciding when a class
is constructed is no longer the one who wrote that class, it is the container itself.

This lesson **models** that container — no real framework's name, annotation syntax, or
behavior is used here; what is built is only a small class written with the standard library's
own reflection (`java.lang.reflect`) and annotation type (`java.lang.annotation`) tools. The
question asked stays the same throughout the course: did a **call** in the source produce a
behavior, an **annotation**, or a **default** that leaves no trace in the source at all?

## What the Container Models

The `Container` class built below recognizes four annotation types: **`Component`**, which
marks that a class falls under the container's management and carries an optional `scope`;
**`Init`**, which requests that a method be called once the object is ready; **`Wrap`**, which
requests that a class be placed behind a call-counting proxy; **`Value`**, which requests that a
field be filled with a value supplied from outside. All four are the container's own
definitions — this lesson's vocabulary is contributed by this course, not by
`docs/TERMINOLOGY.md`.

The object the container manages is referred to in this course as a **managed component**. A
managed component is nothing more than an instance of a class carrying the `Component` annotation; the
container constructs it, resolves the dependencies it needs through the constructor, calls a
method marked `Init` if one exists, and keeps the same instance if `scope` is singleton. A
managed component's construction follows these steps: find the constructor, resolve its
parameters in order (since each parameter can itself be a managed component, this step calls
itself), create the instance, fill the fields marked `Value`, call the method marked `Init`, and
if marked `Wrap`, place it behind a proxy. This lesson measures only the first two steps in
depth — construction and scope; `Init`, `Value`, and `Wrap` are the subjects of this topic's
third, fourth, and sixth lessons respectively, and appear here only as part of the model.

- **CC1.** The IoC container is built here as a model; no real framework's name, annotation
  syntax, or API is used. `Component`, `Init`, `Wrap`, `Value` are this lesson's own annotation
  types.
- **CC2.** Identity is compared only with `==`; no object's identity hash is ever printed.
- **CC3.** Singleton scope is the container's default scope; every managed component is a
  singleton unless a different one is explicitly requested with an annotation.
- **CC4.** Constructor dependencies are resolved recursively by the container; supplying a
  dependency through a field is not built in this topic.
- **CC5.** The measurement contains no real network, database, or file system call; the store is
  a field held in memory.

## Construction Invisible in the Source

The program below builds a small two-component system: `Store` is an interface, `InMemoryStore`
is the only class implementing it, and `Service` is a second managed component depending on
`Store`. Nowhere is `new InMemoryStore()` or `new Service(...)` written — these objects appear on
their own when the container's `get` method is called. The second third of the program compares
this same `InMemoryStore`, built by hand, against the one built by the container; the third
third records where a broken annotation lands.

```java
// Container.java — did a call in the source produce this behavior, an annotation, or the container's own default
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

public class Container {

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
    @interface Component { String scope() default "singleton"; }

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
    @interface Init {}

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
    @interface Wrap {}

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
    @interface Value { String name(); String defaultValue(); }

    static int call = 0, annotation = 0, byDefault = 0;
    static int error = 0, silent = 0, ineffective = 0;

    static void observe(String behavior, String source) {
        switch (source) {
            case "call" -> call++;
            case "annotation" -> annotation++;
            default -> byDefault++;
        }
        System.out.printf("%-38s %s%n", behavior, source);
    }

    static void rule(String name, String result) {
        switch (result) {
            case "error" -> error++;
            case "silent" -> silent++;
            default -> ineffective++;
        }
        System.out.printf("%-38s %s%n", name, result);
    }

    static void summary() {
        System.out.printf("behavior: %d call, %d annotation, %d default%n",
                call, annotation, byDefault);
        System.out.printf("defect  : %d error, %d silent, %d ineffective%n", error, silent, ineffective);
    }

    final Map<Class<?>, Object> singletons = new HashMap<>();
    final Map<String, String> settings = new LinkedHashMap<>();
    final List<String> log = new ArrayList<>();
    int wrapCalls = 0;

    final Map<Class<?>, Class<?>> bindings = new HashMap<>();

    Container configure(String name, String value) { settings.put(name, value); return this; }

    Container bind(Class<?> iface, Class<?> implementation) { bindings.put(iface, implementation); return this; }

    Object get(Class<?> type) throws Exception {
        if (type.isInterface()) {
            Class<?> implementation = bindings.get(type);
            if (implementation == null)
                throw new IllegalStateException("no implementation bound to interface: " + type.getSimpleName());
            return get(implementation);
        }
        Component c = type.getAnnotation(Component.class);
        if (c == null) throw new IllegalStateException("no component annotation: " + type.getSimpleName());
        boolean singleton = c.scope().equals("singleton");
        if (singleton && singletons.containsKey(type)) return singletons.get(type);

        Constructor<?> constructor = type.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        Object[] arg = new Object[constructor.getParameterCount()];
        for (int i = 0; i < arg.length; i++) arg[i] = get(constructor.getParameterTypes()[i]);
        Object instance = constructor.newInstance(arg);

        for (Field f : type.getDeclaredFields()) {
            Value v = f.getAnnotation(Value.class);
            if (v == null) continue;
            f.setAccessible(true);
            f.set(instance, settings.getOrDefault(v.name(), v.defaultValue()));
        }
        for (Method m : type.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Init.class)) { m.setAccessible(true); m.invoke(instance); log.add("init:" + type.getSimpleName()); }
        }
        if (type.isAnnotationPresent(Wrap.class) && type.getInterfaces().length == 1) {
            Object target = instance;
            instance = Proxy.newProxyInstance(type.getClassLoader(), type.getInterfaces(),
                    (p, m, a) -> { wrapCalls++; return m.invoke(target, a); });
        }
        if (singleton) singletons.put(type, instance);
        return instance;
    }

    String tryGet(Class<?> type) {
        try { get(type); return "ok"; }
        catch (Exception e) { return e.getClass().getSimpleName(); }
    }

    public static void main(String[] args) throws Exception {
        Container container = new Container().bind(Store.class, InMemoryStore.class).configure("store.startup", "full");

        System.out.println("-- what produced the behavior --");

        Service service = (Service) container.get(Service.class);
        Container.observe("object construction", "annotation");
        Container.observe("dependency supplied", service.store != null ? "annotation" : "call");
        Container.observe("same instance returned", container.get(Service.class) == service ? "default" : "call");
        Container.observe("new instance every call", container.get(Request.class) != container.get(Request.class) ? "annotation" : "default");
        Container.observe("init method called", container.log.contains("init:InMemoryStore") ? "annotation" : "call");

        Container empty = new Container().bind(Store.class, InMemoryStore.class);
        Service defaulted = (Service) empty.get(Service.class);
        Container.observe("setting's default value", defaulted.report().equals("ready:empty") ? "default" : "call");
        Container.observe("setting given from outside", service.report().equals("ready:full") ? "annotation" : "default");

        int previousWraps = container.wrapCalls;
        service.report();
        Container.observe("call wrapped", container.wrapCalls > previousWraps ? "annotation" : "default");

        InMemoryStore manual = new InMemoryStore();
        Container.observe("wrapping on a manually built object", Proxy.isProxyClass(manual.getClass()) ? "annotation" : "default");
        Container.observe("direct method call", manual.read().equals("not initialized") ? "call" : "annotation");
        Container.observe("manual construction with new", "call");
        Container.observe("dependency given by hand", new Service(manual).report().equals("not initialized") ? "call" : "annotation");

        System.out.println("-- where annotation defects land --");

        Container.rule("unannotated type requested", container.tryGet(Unannotated.class).equals("ok") ? "silent" : "error");

        Container k2 = new Container().bind(Store.class, InMemoryStore.class).configure("store.startup", "full");
        WrongSetting ws = (WrongSetting) k2.get(WrongSetting.class);
        Container.rule("setting name misspelled", ws.value.equals("full") ? "ineffective" : "silent");
        System.out.println("expected value full, seen           : " + ws.value);

        Object y1 = k2.get(WrongScope.class), y2 = k2.get(WrongScope.class);
        Container.rule("scope name not recognized", y1 == y2 ? "ineffective" : "silent");

        Object noIface = k2.get(InterfacelessComponent.class);
        Container.rule("wrap put on an interfaceless class", noIface instanceof Proxy ? "error" : "ineffective");

        Container.summary();
    }
}

interface Store { String read(); }

@Container.Component @Container.Wrap
class InMemoryStore implements Store {
    @Container.Value(name = "store.startup", defaultValue = "empty") String startup;
    String status = "not initialized";
    @Container.Init void prepare() { status = "ready:" + startup; }
    public String read() { return status; }
}

@Container.Component
class Service {
    final Store store;
    Service(Store store) { this.store = store; }
    String report() { return store.read(); }
}

@Container.Component(scope = "prototype")
class Request { }

class Unannotated { }

@Container.Component @Container.Wrap
class InterfacelessComponent { String hello() { return "hello"; } }

@Container.Component(scope = "undefined-scope")
class WrongScope { }

@Container.Component
class WrongSetting {
    @Container.Value(name = "store-startup", defaultValue = "empty") String value;
}
```

```
-- what produced the behavior --
object construction                    annotation
dependency supplied                    annotation
same instance returned                 default
new instance every call                annotation
init method called                     annotation
setting's default value                default
setting given from outside             annotation
call wrapped                           annotation
wrapping on a manually built object    default
direct method call                     call
manual construction with new           call
dependency given by hand               call
-- where annotation defects land --
unannotated type requested             error
setting name misspelled                silent
expected value full, seen           : empty
scope name not recognized              silent
wrap put on an interfaceless class     ineffective
behavior: 3 call, 6 annotation, 3 default
defect  : 1 error, 2 silent, 1 ineffective
```

## Three Sources, Twelve Behaviors

The output's first line was never printed by any code — the call `Container.observe("object
construction", "annotation")` just gives a fixed string, because the `service` variable had
already returned from the `container.get(Service.class)` call, and what produced that call was
not a line in the source at all, it was the `@Container.Component` annotation on the `Service`
class. This topic's opening claim sits exactly here: constructing an object is not an **action**,
it is an **effect**. Whoever reads the program sees the line
`Service service = (Service) container.get(Service.class);`, but this line does not say how
`Service` is to be constructed — it only asks the container for an instance. The construction
decision itself sits in the annotation on `Service.class`.

The second line — `container.get(Service.class) == service` — makes the same request twice and
gets the same object both times. This is the container's **default** decision: when the
`Component` annotation does not specify a `scope`, the container chooses singleton scope, and
this choice is not written anywhere in the source. We can see the value `scope = "singleton"` on
the line `@interface Component { String scope() default "singleton"; }`, but the `Service` class
itself never mentions this value at all — the annotation is bare `@Container.Component`, with no
parameter. Knowing where the default comes from requires looking at the container's own source
code; looking at `Service.java` is not enough.

The final third of the program builds the same `InMemoryStore` class by hand:
`InMemoryStore manual = new InMemoryStore();`. This line really does stand in the source as a
**call** — and it produces an object that differs from the one the container builds in three
ways. The output's final call line traces back to the same root: `new Service(manual)` is also a
manually written dependency handoff; the container never steps in at all. The three `call` lines
standing in the source — a direct method call, a manual `new`, a manual dependency handoff — all
confirm the same observation: every line that bypasses the container is a line that really
appears in the source.

The remaining behaviors are the subject of this topic's later lessons and are only pointed to
here: "dependency supplied" and "dependency given by hand" are the second lesson's (Dependency
Injection), "new instance every call" and "init method called" are the third lesson's (Component
Scopes and Lifecycle), "setting's default value" and "setting given from outside" are the fourth
lesson's (Configuration Sources), "call wrapped" is the sixth lesson's (Aspect-Oriented
Programming). Of the twelve behaviors, three come from a call in the source, six from an
annotation, three from the container's own default — someone reading the source from top to
bottom cannot see three-quarters of these behaviors.

## The Object Left Outside the Container

The manually built `InMemoryStore` never received three things: the `prepare` lifecycle hook was
never called (the `status` field still reads `"not initialized"`), no `Value` was placed into the
`startup` field (the field stayed `null`; the `read()` method never uses it so it did not show up
in the output, but this line does not observe that at all), and the object was not placed behind
a proxy — `Proxy.isProxyClass(manual.getClass())` returned `false`. All three of these are the
container's **default**, but a strange kind of default: the container never getting involved at
all, the class staying in its own bare form. The annotation keeps standing on the class —
`InMemoryStore` is still `@Container.Component`, still `@Container.Wrap` — but there is no one
reading it.

This topic's rule is: **an annotation is a request, not a feature**; when no container exists
to fulfill the request, nothing happens at all — no error, no warning. The `manual` object is a
completely valid Java object, produced without throwing any exception, carrying no defect flag —
it only did not receive the three things the container was supposed to give. This is not a
separate defect from the behaviors measured through this lesson; it is the container's own
boundary. Everything an IoC container gives exists only to the extent that container gave it.

The lesson's second rule block shows a different defect: the `Unannotated` class carries no
`@Container.Component` at all, and the container throws an exception immediately when an
instance is requested from it — this is the only **noisy** one of this course's four defects.
The defect falls not at the line where the container is built, but at the moment
`container.get(Unannotated.class)` is called — that is, at the **first request**: the
`new Container()` line itself performs no check at all, it does not scan in advance which
classes are annotated. The other three defects — a misspelled setting name, an unrecognized
scope name, a wrap marker placed on an interfaceless class — are measured in depth in this
topic's later lessons; here they only appear in the summary line.

## Summary

- Inversion of control is the decision of when and how an object is constructed passing from the
  writing side to an IoC container; the container reads this decision from annotations on the
  class.
- Making the same request twice gives the same instance — this is the container's singleton
  scope default, and it is written nowhere in the source.
- An object built by hand with `new` arises from a call that really stands in the source, and
  never enters the container's management at all.
- An object left outside the container receives none of the annotations on it, even though
  those annotations keep standing there — an annotation is a request, and if there is no one to
  read it, it goes unanswered.
- When a type with no annotation is requested, the container throws an exception immediately and
  by name; this defect falls not at the moment the container is built, but at the moment of the
  first request.
- Of twelve measured behaviors, three come from a call in the source, six from an annotation,
  three from the container's own default.

## Next Step

The manual `new Service(manual)` line was actually hand-imitating one of the most important
mechanisms inside the container: giving a dependency to a constructor. The container does this
by looking at `Service`'s single constructor parameter, requesting that parameter's type, and
passing in the returned object — none of these lines are in the source, but the result is the
same. The next lesson measures this mechanism itself: why the difference between a dependency
being supplied and being given by hand is not just a readability preference, but a testable
design decision.
