If you have written any Spring Boot code, you have seen this: a class asks for another class in its constructor, and somehow the right object is just there. Nobody called new. Nobody looked anything up. Let's see who does that work, with plain words and a tiny Spring Boot app you can run yourself.
The idea in one minute
- You mark classes with annotations like
@Serviceor@Component. - When the app starts, Spring Boot scans your code and creates an object of each one. These managed objects are called beans, and they live in the Spring container (the
ApplicationContext). - When a bean's constructor asks for another bean, the container passes it in. That passing-in is dependency injection.
The Spring documentation calls this Inversion of Control: instead of your object building or finding its own helpers, the container creates them and hands them over. Your class just declares what it needs.
Watch the flow
The animation follows the real program below: main() hands control to Spring Boot, the scan finds the two annotated classes, the container creates the GreetingService bean, creates WelcomeRunner while injecting GreetingService into its constructor, and run() uses the injected bean.
Run it yourself
A complete Spring Boot app in one file (run with mvn compile exec:java -Dexec.mainClass=com.example.di.DiDemoApplication in a project with the spring-boot-starter dependency, Spring Boot 3.5.7):
package com.example.di; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; @SpringBootApplication public class DiDemoApplication { // 1. A bean the container will create and manage @Service static class GreetingService { GreetingService() { System.out.println("[container] created bean: GreetingService"); } String greet(String name) { return "Hello, " + name + "!"; } } // 2. A bean that NEEDS a GreetingService: constructor injection @Component static class WelcomeRunner implements CommandLineRunner { private final GreetingService greetingService; WelcomeRunner(GreetingService greetingService) { // container injects it here this.greetingService = greetingService; System.out.println("[container] injected GreetingService into WelcomeRunner"); } @Override public void run(String... args) { System.out.println("[app] WelcomeRunner.run() called with the injected bean"); System.out.println("[app] " + greetingService.greet("Spring Boot")); } } public static void main(String[] args) { System.out.println("[you] main(): handing control to SpringApplication.run()"); SpringApplication.run(DiDemoApplication.class, args); System.out.println("[you] main(): container shut down"); } } What it actually printed (Spring Boot 3.5.7, Java 21)
Real run, startup banner and framework log lines trimmed:
[you] main(): handing control to SpringApplication.run() :: Spring Boot :: (v3.5.7) ... Starting DiDemoApplication using Java 21.0.13 ... [container] created bean: GreetingService [container] injected GreetingService into WelcomeRunner ... Started DiDemoApplication in 0.774 seconds ... [app] WelcomeRunner.run() called with the injected bean [app] Hello, Spring Boot! [you] main(): container shut down Read it in order and you can see the handoff:
- Your
main()does exactly one thing: start the container. - The container's log lines come before the app's: it creates
GreetingService, then buildsWelcomeRunnerand passes the service into the constructor. All before "Started DiDemoApplication". - Only then does
run()execute, already holding a ready-to-use bean.CommandLineRunneris just Spring Boot's way of saying "call this once everything is wired up".
What the docs add
- Two ways to inject. The Spring Framework docs describe constructor-based and setter-based DI. The rule of thumb they give: constructors for mandatory dependencies, setters for optional ones.
- Constructor injection is the recommended style. The Spring team advocates it because components stay immutable, required dependencies can never be
null, and the object is always fully initialized when you get it. The Spring Boot docs give the same advice: use constructor injection plus@ComponentScan, which@SpringBootApplicationalready includes. - It pays off in tests. Because your class receives its helpers instead of building them, a test can hand it a stub or mock instead of the real thing.
One honest caveat: the container does much more than this tiny demo shows (scopes, lazy init, configuration properties, proxies). But the core loop you saw above - scan, create, inject, use - is the part everything else builds on.
Sources
- Dependency Injection - Spring Framework Documentation
- Spring Beans and Dependency Injection - Spring Boot Documentation
Written with AI assistance; code examples were compiled and run on Spring Boot 3.5.7 with JDK 21 before publishing.
