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.
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
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
public class Person {
private String firstName;
private String lastName;
// constructor, setter, getter...
}
@ComponentScan("your_package")
public class Config {}@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
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 objectSingleton 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
@Component
public class Car {
private final Person person;
public Car(Person person) { // no @Autowired needed
this.person = person;
}
}@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.
Thymeleaf for Spring Boot: From th:text to Fragments
NextJWT Authentication: Anatomy of a Filter Chain
Related articles
MyBatis Dynamic SQL: The Tags That Actually Matter
if, where, set, foreach, and choose cover almost every real query I write. A field guide with the patterns I reach for most.
Thymeleaf for Spring Boot: From th:text to Fragments
A practical path through Thymeleaf — basic attribute binding, object selection, and the fragment pattern that kills copy-pasted navbars.
REST API Design: DTOs, Exception Handling, and CORS
Three unglamorous decisions that separate a REST API a frontend team can actually use from one they'll keep filing bugs against: what you return, how you fail, and who's allowed to call you.