Skip to content
IntermediateBackend Notes

Inversion of Control & Dependency Injection: The Bean I Kept Recreating

Six classes, one Person object, one `new Person(...)` copy-pasted six times. That's the problem IoC actually solves — not an abstract principle, a concrete duplication bug.

Hen HeangJuly 10, 20267 min read
Spring BootJavaArchitecture

Inversion of Control sounds like an abstract architecture principle until you hit the concrete problem it solves: six classes all need the same Person object, so six classes each call `new Person("Jon", "Snow")`. Now you want to change the name — you're editing six places. That's the actual motivating problem, not a diagram.

The problem before IoC

java
public class ClassA {
    public static void main(String[] args) {
        Person person = new Person("Jon", "Snow");
        System.out.println(person);
    }
}
// ClassB, ClassC, ClassD... each need their own "Jon Snow"
// -> each one does its own "new Person(\"Jon\", \"Snow\")"

Info

In IoC, objects do not create the other objects they depend on. Instead, a container creates and configures those dependencies externally, then injects them into whatever needs them — one Person, shared, instead of six copies drifting apart.

Two ways to register a bean

Component + ComponentScan
@Component
public class Person {
    private String firstName;
    private String lastName;
    // constructor, setter, getter...
}

@ComponentScan("your_package")
public class Config {}
Configuration + Bean
@Configuration
public class Config {
    @Bean
    public Person person() {
        return new Person("jon", "snow");
    }
}

@Component tells the container to create one bean of that type automatically during a classpath scan. @Configuration + @Bean is more explicit — a method you write yourself decides how the bean gets constructed, which matters when construction needs logic beyond a plain constructor call.

Singleton by default — and why that surprises people

java
Person personOne = applicationContext.getBean(Person.class);
Person personTwo = applicationContext.getBean(Person.class);
personOne.setFirstName("Kok");

System.out.println(personOne); // Person(firstName=Kok, lastName=Son)
System.out.println(personTwo); // Person(firstName=Kok, lastName=Son) -- same object

Singleton is the default scope

Every bean in Spring is a singleton unless you mark it @Scope("prototype"). personOne and personTwo above are the exact same instance — mutating one mutates both. This is the source of a specific class of bug: storing per-request state on a singleton service field.

Three ways to inject a dependency

Constructor injection
@Component
public class Car {
    private final Person person;

    public Car(Person person) { // no @Autowired needed
        this.person = person;
    }
}
Field injection — not recommended
@Component
public class Car {
    @Autowired
    private Person person; // required dependencies are invisible from outside the class
}

Spring's own recommendation

Constructor injection for mandatory dependencies, setter injection for optional ones. Field injection is the one to avoid: the list of required dependencies is invisible from outside the class, the object can be constructed in a half-initialized state, and it's harder to unit test without a Spring context.

That single duplication problem — six classes, one Person, one hardcoded value drifting out of sync — is the whole justification for the container, the bean, and every annotation built around them. Everything else is mechanics.