Command Palette

Search for a command to run...

[Spring Boot Basics] IoC and Dependency Injection in Spring: Why You Stop Calling new

This article opens Chapter 1, and it is the one the rest of the series stands on. Spring is, at bottom, a container that builds your objects for you. Everything else — stereotypes, auto-configuration, transactions, security — is built on that single fact. If the fact never lands, the rest becomes a list of annotations you memorise and cannot debug.

So this is not a tour of annotations. It is one small object graph, written three times: with new at every level, then wired by hand in main with no framework at all, then wired by Spring. Every listing below was compiled and run on OpenJDK 21.0.6 with Spring Boot 4.1.1 (Spring Framework 7.0.9) and Gradle 9.7.1; every program output, stack trace, compiler message and test report is copied from the terminal.

The new keyword struck out, and a class receiving its collaborators from outside

The three versions live in three packages of the same project — com.example.demo.selfbuilt, com.example.demo.wired and com.example.demo.order — so you can keep all three side by side and run whichever you like.

An object graph built with new

Four classes, which is roughly the smallest graph that is still honest about a real application. A controller needs a service, the service needs a repository, the repository needs some configuration, and the configuration comes from the environment.

DataSourceConfig.java
public class DataSourceConfig {
 
    private final String url;
 
    public DataSourceConfig() {
        String value = System.getenv("ORDERS_DB_URL");
        if (value == null) {
            throw new IllegalStateException("ORDERS_DB_URL is not set");
        }
        this.url = value;
        System.out.println("    built DataSourceConfig -> " + value);
    }
 
    public String url() {
        return url;
    }
}
JdbcOrderRepository.java
public class JdbcOrderRepository {
 
    private final DataSourceConfig config = new DataSourceConfig();
 
    public JdbcOrderRepository() {
        System.out.println("    built JdbcOrderRepository on " + config.url());
    }
 
    public List<Order> findByCustomer(String customer) {
        // a real SELECT against config.url() would go here
        return List.of(new Order("A-1", customer, 2500), new Order("A-2", customer, 1750));
    }
}
OrderService.java
public class OrderService {
 
    private final JdbcOrderRepository repository = new JdbcOrderRepository();
 
    public OrderService() {
        System.out.println("    built OrderService");
    }
 
    public long totalFor(String customer) {
        return repository.findByCustomer(customer).stream().mapToLong(Order::amountCents).sum();
    }
}
OrderController.java
public class OrderController {
 
    private final OrderService service = new OrderService();
 
    public OrderController() {
        System.out.println("    built OrderController");
    }
 
    public String total(String customer) {
        return customer + " owes " + service.totalFor(customer) + " cents";
    }
}

There is nothing exotic here. It is the code almost everyone writes first, and it works:

Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println("asking for one controller:");
        OrderController controller = new OrderController();
        System.out.println(controller.total("ada"));
    }
}
Text
asking for one controller:
    built DataSourceConfig -> jdbc:postgresql://localhost:5432/orders
    built JdbcOrderRepository on jdbc:postgresql://localhost:5432/orders
    built OrderService
    built OrderController
ada owes 4250 cents

That output is already the whole problem, printed. Read it again: the program asked for one object and got four. Three consequences follow, and none of them is a matter of taste.

Building the top of the graph builds all of it

new OrderController() is one statement, and it constructed a database configuration. The controller does not want a DataSourceConfig — it does not mention one, does not import one, could not name one — yet it cannot exist without one. The dependency is not declared anywhere; it is welded in three levels down.

Remove the environment variable and the welding becomes visible as a stack trace:

Text
asking for one controller:
Exception in thread "main" java.lang.IllegalStateException: ORDERS_DB_URL is not set
	at com.example.demo.selfbuilt.DataSourceConfig.<init>(DataSourceConfig.java:10)
	at com.example.demo.selfbuilt.JdbcOrderRepository.<init>(JdbcOrderRepository.java:7)
	at com.example.demo.selfbuilt.OrderService.<init>(OrderService.java:5)
	at com.example.demo.selfbuilt.OrderController.<init>(OrderController.java:5)
	at com.example.demo.selfbuilt.Main.main(Main.java:6)

Four <init> frames for one new. Construction order has leaked out of each class and into the type graph: the only way to make OrderController exist is to make the whole tree beneath it succeed first, in exactly that order, and every caller pays for it whether it needs the bottom of the tree or not.

Swapping an implementation means editing the consumer

Now suppose you want an in-memory repository — for a test, for a demo, for a customer who does not have Postgres. Write one:

InMemoryOrderRepository.java
public class InMemoryOrderRepository {
 
    public List<Order> findByCustomer(String customer) {
        return List.of(new Order("M-1", customer, 999));
    }
}

and try to give it to the service:

Java
OrderService service = new OrderService(new InMemoryOrderRepository());
Text
SwapAttempt.java:5: error: constructor OrderService in class OrderService cannot be applied to given types;
        OrderService service = new OrderService(new InMemoryOrderRepository());
                               ^
  required: no arguments
  found:    InMemoryOrderRepository
  reason: actual and formal argument lists differ in length
1 error

There is no way in. OrderService has no opening through which a different repository can arrive, because it decided for itself what its repository would be. The only fix is to edit OrderService — a class whose behaviour you did not want to change — and every consumer that wants a different choice forces another edit into the same file. Two callers with two different needs cannot both be satisfied.

One method cannot be tested without the whole graph

totalFor is four tokens of arithmetic over a list. Here is a perfectly ordinary JUnit 5 test of it:

OrderServiceTest.java
class OrderServiceTest {
 
    @Test
    void totalsTheCustomersOrders() {
        OrderService service = new OrderService();
        assertEquals(4250, service.totalFor("ada"));
    }
}
Text
> Task :test FAILED
 
OrderServiceTest > totalsTheCustomersOrders() FAILED
    java.lang.IllegalStateException at OrderServiceTest.java:11
 
1 test completed, 1 failed

The report says what happened:

Text
java.lang.IllegalStateException: ORDERS_DB_URL is not set
	at com.example.demo.selfbuilt.DataSourceConfig.<init>(DataSourceConfig.java:10)
	at com.example.demo.selfbuilt.JdbcOrderRepository.<init>(JdbcOrderRepository.java:7)
	at com.example.demo.selfbuilt.OrderService.<init>(OrderService.java:5)
	at com.example.demo.selfbuilt.OrderServiceTest.totalsTheCustomersOrders(OrderServiceTest.java:11)

A test for a sum failed on a database URL. That is the cost of the first two problems arriving together: to reach one method you must construct the entire graph, and the graph is not negotiable. In a real codebase this is the moment someone sets up a test database, or a Docker container, or @SpringBootTest on a unit test — a large amount of machinery bought to compensate for one new.

The same four objects drawn twice: nested inside one another versus separate and received through constructors

Inversion of Control is the principle

The fix has a name, and it is worth separating the name from its implementation, because most tutorials blur them into one word.

Inversion of Control is a general design principle: the flow of control moves from your code to the framework. Ordinarily your program is the caller — it starts at main, decides what to build, calls a library, and drives the process from beginning to end. Under Inversion of Control, something else drives, and your code is what gets called. This is the Hollywood Principle: do not call us, we will call you.

You have already been living under it. A servlet container calls your doGet; JUnit calls your @Test methods; a Comparator you pass to sort is called by the sort, not by you. In every case you supplied code and something else decided when it ran.

What Spring inverts is narrower and more specific: construction and lookup. You no longer write the code that builds objects and finds collaborators. The container builds them, hands them to each other, and calls into your classes when there is work to do.

Your main driving construction, versus the container driving it and calling into your code

Two things follow from the picture that are easy to miss.

Inversion of Control is bigger than Spring and bigger than dependency injection. A template method that calls your override, an event loop that calls your handler, and a build tool that calls your task are all inversions of control with no container anywhere.

And Inversion of Control is a principle, not a mechanism. It tells you what should be true, not how to achieve it. The mechanism is the next section.

Dependency Injection is the technique

Dependency Injection is the specific technique that implements Inversion of Control for object construction: an object receives its collaborators from outside instead of creating them. Nothing more than that. Martin Fowler coined the name in 2004 precisely because "Inversion of Control" was too broad to say which inversion people meant.

In code it is a one-line change per class — the field stops being an initialiser and becomes a parameter:

OrderService.java
public class OrderService {
 
    private final OrderRepository repository;
 
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
 
    public long totalFor(String customer) {
        return repository.findByCustomer(customer).stream().mapToLong(Order::amountCents).sum();
    }
}

Three things changed, and each earns its place.

The field type is now the interface OrderRepository, so the service is coupled to a capability rather than to a class. The collaborator arrives as a constructor parameter, which makes the dependency part of the public signature — you can no longer build an OrderService without admitting that it needs a repository. And because the parameter is assigned once, the field can be final, so the object is fully formed the instant it exists or it does not exist at all.

That constructor parameter is a seam: a place where the object graph can be taken apart and put together differently. Everything good in the rest of this article comes from that one seam existing.

There are three places an object can receive a dependency — the constructor, a setter and the field itself — and which one to prefer is a real argument with a right answer. It is the subject of the next article but one; this whole article uses constructor injection and nothing else.

Wiring the same graph by hand, with no framework

Before Spring appears, do the intermediate step, because this is what makes the idea land. Give all four classes constructor parameters, and then write the wiring yourself:

Assembly.java
public class Assembly {
 
    public static void main(String[] args) {
        System.out.println("assembling by hand:");
 
        String url = System.getenv().getOrDefault("ORDERS_DB_URL", "jdbc:postgresql://localhost:5432/orders");
 
        DataSourceConfig config = new DataSourceConfig(url);
        OrderRepository repository = new JdbcOrderRepository(config);
        OrderService service = new OrderService(repository);
        OrderController controller = new OrderController(service);
 
        System.out.println(controller.total("ada"));
    }
}

Compile it with nothing but javac and run it with nothing but java:

Bash
javac -d out $(find src/main/java/com/example/demo/wired -name '*.java')
java -cp out com.example.demo.wired.Assembly
Text
assembling by hand:
    built DataSourceConfig -> jdbc:postgresql://localhost:5432/orders
    built JdbcOrderRepository on jdbc:postgresql://localhost:5432/orders
    built OrderService with JdbcOrderRepository
    built OrderController
ada owes 4250 cents

The classpath is eight class files and not one line of any framework:

Text
out/com/example/demo/wired/Assembly.class
out/com/example/demo/wired/DataSourceConfig.class
out/com/example/demo/wired/InMemoryOrderRepository.class
out/com/example/demo/wired/JdbcOrderRepository.class
out/com/example/demo/wired/Order.class
out/com/example/demo/wired/OrderController.class
out/com/example/demo/wired/OrderRepository.class
out/com/example/demo/wired/OrderService.class

Look at what is already fixed. Nothing but main knows the shape of the graph, so no class builds anything it does not use. OrderService can be handed any OrderRepository, so it is testable without a database. Swapping the implementation is one line, in one file, and no class that uses the repository is touched:

Java
OrderRepository repository = new InMemoryOrderRepository();
OrderService service = new OrderService(repository);
OrderController controller = new OrderController(service);
Text
assembling by hand:
    built InMemoryOrderRepository (no database)
    built OrderService with InMemoryOrderRepository
    built OrderController
ada owes 999 cents

Every benefit in this article is already present, and Spring has not been mentioned. Dependency injection is a design technique, not a framework feature. What is left is one thing only: this main has four lines for four objects, and a real application has hundreds of objects, in a dependency order nobody wants to maintain by hand, where inserting one new collaborator means editing the assembly of everything downstream of it.

That tedium is the container's entire job. Spring is not the idea; Spring is the machine that does the typing.

The same graph, wired by Spring

The same four classes, with one annotation each and not one line of wiring:

DataSourceConfig.java
@Component
public class DataSourceConfig {
 
    private final String url;
 
    public DataSourceConfig() {
        this.url = System.getenv().getOrDefault("ORDERS_DB_URL", "jdbc:postgresql://localhost:5432/orders");
    }
 
    public String url() {
        return url;
    }
}
JdbcOrderRepository.java
@Repository
public class JdbcOrderRepository implements OrderRepository {
 
    private final DataSourceConfig config;
 
    public JdbcOrderRepository(DataSourceConfig config) {
        this.config = config;
    }
 
    @Override
    public List<Order> findByCustomer(String customer) {
        return List.of(new Order("A-1", customer, 2500), new Order("A-2", customer, 1750));
    }
}
OrderService.java
@Service
public class OrderService {
 
    private final OrderRepository repository;
 
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
 
    public long totalFor(String customer) {
        return repository.findByCustomer(customer).stream().mapToLong(Order::amountCents).sum();
    }
}
OrderController.java
@RestController
public class OrderController {
 
    private final OrderService service;
 
    public OrderController(OrderService service) {
        this.service = service;
    }
 
    @GetMapping("/orders/{customer}/total")
    public String total(@PathVariable String customer) {
        return customer + " owes " + service.totalFor(customer) + " cents";
    }
}

Four annotations mark the classes the container is allowed to build; what each one means and how they are found is the next article's subject, and the only rule you need today is the one from the previous article — they must live under the package of your @SpringBootApplication class. Note also what is not here: no @Autowired. Since Spring Framework 4.3, a class with exactly one constructor has that constructor used for injection automatically. That annotation, and what happens when the container has to choose between candidates, belongs to the article after next.

DataSourceConfig still reads the environment itself here; moving that job into configuration is what @Configuration and @Bean are for, two articles from now.

Proving the container did the wiring

It is easy to assert that Spring wires things. It takes about one line per class to show it. Each constructor prints its own identity and the identity of what it received:

Java
public OrderService(OrderRepository repository) {
    this.repository = repository;
    System.out.println("    OrderService#" + Integer.toHexString(System.identityHashCode(this))
            + " built, received " + repository.getClass().getSimpleName()
            + "#" + Integer.toHexString(System.identityHashCode(repository)));
}
Bash
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --server.port=8085
Text
o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8085 (http)
b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 271 ms
    DataSourceConfig#57402ba1 built, url=jdbc:postgresql://localhost:5432/orders
    JdbcOrderRepository#5c534b5b built, received DataSourceConfig#57402ba1
    OrderService#14229fa7 built, received JdbcOrderRepository#5c534b5b
    OrderController#7158daf2 built, received OrderService#14229fa7
o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8085 (http)
com.example.demo.DemoApplication         : Started DemoApplication in 0.56 seconds

Read the identities down the chain. DataSourceConfig#57402ba1 is built first; the repository reports receiving that exact instance; the service reports receiving the repository the previous line built; the controller reports receiving that service. The container constructed the graph bottom-up, in dependency order, and passed each object into the next — the same four lines Assembly.main executed, produced by nobody writing them.

Three details in that log are worth naming. The order is dependency order, not source order or alphabetical order: Spring works out what each constructor needs and builds that first. All of it happens at startup, before the port opens, so by the time a request can arrive the graph is finished. And repository.getClass().getSimpleName() printed JdbcOrderRepository, not some $Proxy name — what was injected is the plain object, not a wrapper.

The endpoint works, which is the least interesting part of all this:

Bash
curl -s http://localhost:8085/orders/ada/total
Text
ada owes 4250 cents

What dependency injection actually buys you

Three payoffs, each demonstrated rather than asserted.

Testability: a stub at the constructor seam

The seam accepts anything that implements the interface, so a test can supply seven lines of Java instead of a database:

OrderServiceTest.java
class OrderServiceTest {
 
    /** A stub repository: seven lines, no database, no Spring. */
    static class StubOrderRepository implements OrderRepository {
        @Override
        public List<Order> findByCustomer(String customer) {
            return List.of(new Order("S-1", customer, 1000), new Order("S-2", customer, 250));
        }
    }
 
    @Test
    void totalsTheCustomersOrders() {
        OrderService service = new OrderService(new StubOrderRepository());
        assertEquals(1250, service.totalFor("ada"));
    }
 
    @Test
    void totalsZeroWhenTheCustomerHasNoOrders() {
        OrderService service = new OrderService(customer -> List.of());
        assertEquals(0, service.totalFor("nobody"));
    }
}
Text
> Task :test
 
OrderServiceTest > totalsTheCustomersOrders() PASSED
 
OrderServiceTest > totalsZeroWhenTheCustomerHasNoOrders() PASSED
 
BUILD SUCCESSFUL in 979ms

There is no @SpringBootTest here, no context, no annotation from the framework at all beyond JUnit's own @Test — the two tests together take 0.013 to 0.022 seconds of test time across repeats, which is indicative rather than a benchmark. The second test is the same point taken to its conclusion: because OrderRepository has one method, it is a functional interface, and the stub can be a lambda.

Compare that with the version of this test in the first section, which needed a database URL to compute 1000 + 250.

The same service with the real repository chain plugged in versus a hand-written stub at the same constructor seam

Swapping an implementation without touching the consumer

Write a second implementation of the same interface and annotate it instead — with the same identity-printing constructor as the others, omitted here for room:

InMemoryOrderRepository.java
@Repository
public class InMemoryOrderRepository implements OrderRepository {
 
    @Override
    public List<Order> findByCustomer(String customer) {
        return List.of(new Order("M-1", customer, 999));
    }
}

Then remove @Repository from JdbcOrderRepository, leaving exactly one candidate, and restart. OrderService.java was not opened — its checksum before and after the change is the same file:

Bash
md5 -q src/main/java/com/example/demo/order/OrderService.java
Text
ed6d5783fbb7c6c5d4dabe69031a7919
ed6d5783fbb7c6c5d4dabe69031a7919
Text
    DataSourceConfig#1b9776f5 built, url=jdbc:postgresql://localhost:5432/orders
    InMemoryOrderRepository#1dd7796b built, no database
    OrderService#a18649a built, received InMemoryOrderRepository#1dd7796b
    OrderController#396639b built, received OrderService#a18649a
Bash
curl -s http://localhost:8085/orders/ada/total
Text
ada owes 999 cents

The behaviour of the endpoint changed and no consumer was edited. Note the first line of the log: DataSourceConfig is still annotated, so the container still builds it even though nothing injects it any more — the container builds what it is told about, not what is reachable.

Leaving both implementations annotated is a different situation: two candidates for one parameter, which the container cannot resolve on its own. Resolving it is what @Primary and @Qualifier are for, and they belong to the article after next.

One place that knows how the application is assembled

In the first version, the knowledge of how the application fits together was smeared across four files, one new at a time, with no single place to look. In the hand-wired version it is in Assembly.main, and you can read the entire architecture of the program in four lines. In the Spring version it is in the container, derived from the constructors themselves.

That is a genuine shift and worth stating plainly: the dependency arrows in your design become the constructor signatures in your code. OrderService(OrderRepository) is not documentation about a design decision; it is the design decision, enforced by the compiler, readable without running anything.

What dependency injection costs you

An honest list, because every one of these is real.

Indirection. repository.findByCustomer(...) no longer tells you which class runs. You read an interface, and the implementation is chosen somewhere you are not looking. On a small project this is pure overhead; the payment comes back only when there is genuinely more than one plausible implementation, or a test that needs to intercept the call.

Failures move from compile time to startup. This is the trade that surprises people most, and it is easy to see both halves. With new, wiring the wrong thing is a compiler error, as in the second section — javac refuses the file. With a container, the same mistake compiles perfectly:

Bash
./gradlew compileJava
Text
> Task :compileJava
 
BUILD SUCCESSFUL in 355ms

and fails when the application starts:

Text
***************************
APPLICATION FAILED TO START
***************************
 
Description:
 
Parameter 0 of constructor in com.example.demo.order.OrderService required a bean of type 'com.example.demo.order.OrderRepository' that could not be found.
 
 
Action:
 
Consider defining a bean of type 'com.example.demo.order.OrderRepository' in your configuration.

Boot works hard to make that message readable, and it names the constructor, the parameter position and the missing type. It is still a runtime error where you used to have a compile error. The mitigation is that it happens at startup rather than on the first request, so it fails in the first second of every test run and every deployment.

⚠️ The failure above was caused by deleting one annotation. Nothing else about the code was wrong, and nothing in the compiler output hinted at it. Wiring errors in a container are configuration errors, and they are only ever found by starting something.

"Where does this bean come from?" In the graph above there are four objects and the answer is obvious. In an application with a few hundred, plus the ones auto-configuration contributes, tracing where a particular instance was created is a real activity with real tooling behind it. That question is exactly what the next few articles teach you to answer.

It is not free to learn. Dependency injection asks a beginner to accept that objects appear from somewhere they cannot see, which is genuinely harder than reading a new. The hand-wired Assembly above is the antidote: when the container confuses you, write the four lines out by hand and the confusion usually resolves.

And it does not require Spring. The wired package proved that with an eight-file classpath. If you take one thing from this article, take that one: dependency injection is a way of writing classes, and Spring is an optional machine for doing the assembly. Applying it to code that will never see a container is still worth doing.

The vocabulary for the rest of this chapter

These eight words are used constantly and rarely defined. This is what they mean here.

TermWhat it means
Inversion of Control (IoC)The principle: the framework calls your code instead of your code calling the framework. Broader than Spring — callbacks, template methods and event loops are all IoC.
Dependency Injection (DI)The technique that implements IoC for object construction: an object receives its collaborators from outside rather than creating them.
ContainerThe thing that does the injecting. It reads your classes, works out what each constructor needs, builds the objects in dependency order and holds them. Spring calls its container the application context.
BeanAn object the container built and manages. The only difference from an ordinary object is who called new.
DependencySomething a class needs in order to work. OrderService depends on OrderRepository. Not to be confused with a build dependency in build.gradle, which is a jar.
CollaboratorThe same relationship named from the other side: the object that gets injected. OrderRepository is OrderService's collaborator.
WiringThe act of deciding which object is passed into which constructor. Done by main in the hand-wired version, by the container in the Spring version.
Object graphAll of your objects plus the references between them. Four nodes and three edges above; a few hundred nodes in a real service.

FAQ

Is Inversion of Control the same thing as Dependency Injection?

No, and the distinction is worth keeping. Inversion of Control is the principle that control flows from the framework to your code; dependency injection is one technique for achieving it, applied specifically to how objects get their collaborators. Every use of DI is IoC, but a servlet container calling doGet, JUnit calling a @Test method, or a sort calling your Comparator are all IoC with no injection involved.

Do I need Spring to do dependency injection?

No. The wired package in this article uses constructor injection throughout and its classpath is eight class files with no framework on it. DI is a way of designing classes; Spring automates the assembly step once the number of objects makes doing it by hand tedious. Plenty of well-designed code applies DI with a hand-written composition root and no container at all.

Why does my constructor work without @Autowired?

Since Spring Framework 4.3, when a class has exactly one constructor the container uses it for injection without any annotation. With two or more constructors it cannot guess, and you must mark the one to use. That annotation and its rules are the subject of the article after next.

Is it wrong to call new in a Spring application?

No — only for things the container should own. Value objects, records, DTOs, exceptions, collections and local variables are created with new constantly and correctly; new Order("A-1", customer, 2500) appears in the listings above. What should not be created with new is a collaborator: a service, a repository, a client, anything with dependencies of its own or a lifetime longer than one method call.

Why did my application compile but fail at startup with "required a bean of type ... that could not be found"?

Because the compiler checks types and the container checks wiring, and they run at different times. The message means a constructor asked for a type that no managed object provides. The usual causes are a missing stereotype annotation on the implementing class, or the class sitting outside the package tree that gets scanned. The next article is about exactly this.

Does going through a container make my application slower?

The container's work happens once, at startup: in the log above, four objects among Boot's own beans, inside a total start time of 0.56 seconds. After that, an injected collaborator is an ordinary final field holding an ordinary reference — the log shows the injected object printing as JdbcOrderRepository rather than a proxy class — so calling it costs exactly what calling any object costs. Features layered on top of the container, such as transactions or security, do add proxies, and those have their own cost; plain injection does not.

Conclusion

An object that calls new on its collaborators has made a decision on behalf of everyone who will ever use it. The demonstration above is that the decision is expensive in three separate ways: one new OrderController() builds four objects and fails on a database URL it never asked about; a different repository cannot be supplied without editing the class that consumes it; and testing one arithmetic method needs the whole graph standing up.

Inversion of Control is the principle that fixes this — the framework calls you, you do not call the framework. Dependency injection is the technique that implements it for construction: collaborators arrive through the constructor, which turns every constructor into a seam. The hand-wired Assembly proves that the seam alone buys you the testability and the swappability, with no framework anywhere. Spring's contribution is to do the assembly for hundreds of objects instead of four, in dependency order, at startup, and to shout when a piece is missing.

The next article opens the container itself: Bean and ApplicationContext — @Component, @Service, @Repository, @Controller, and the component scanning that decides which of your classes the container has ever heard of.

Related Posts

[Spring Boot Basics] Constructor, Setter and Field Injection in Spring, with @Qualifier and @Primary

The three Spring injection points compared by when the container writes the value: why @Autowired is optional on a single constructor, why final fields and plain JUnit 5 tests are only possible with constructor injection, @Autowired(required=false) versus Optional and ObjectProvider, the real NoUniqueBeanDefinitionException fixed four ways with @Primary, @Qualifier, parameter names and a custom qualifier annotation, List and Map injection with @Order, and why a constructor cycle fails at startup while a field cycle does not.

[Spring Boot Basics] Calling External APIs with RestClient in Spring Boot: GET, POST, Error Handling and Timeouts

Calling external HTTP APIs from Spring Boot 4.1.1 with RestClient, checked against a local stub: RestClient vs RestTemplate, WebClient and @HttpExchange, spring-boot-starter-restclient and the auto-configured RestClient.Builder, GET into records and lists, toEntity, query parameter encoding, POST, PUT and DELETE, the real HttpClientErrorException messages, onStatus, defaultStatusHandler and exchange, measured default and configured connect and read timeouts with spring.http.clients, a logging ClientHttpRequestInterceptor, and turning upstream failures into 502, 503 and 504.

[Spring Boot Basics] application.properties vs application.yml in Spring Boot: Syntax and @Value

application.properties and application.yml in Spring Boot 4.1.1, checked by running them: the .properties syntax rules, the ISO-8859-1 default that mangles Vietnamese text, YAML nesting and the SnakeYAML 2.6 values that silently change type, which file wins when both exist, placeholders and random values, and @Value with defaults, type conversion, SpEL and the Environment.

[Spring Boot Basics] Server-Side Rendering with Thymeleaf in Spring Boot: Templates, Forms and Validation

Server-side rendering with Thymeleaf 3.1.5 in Spring Boot 4.1.1, checked on a running application: when SSR beats a JSON API, how a view name becomes classpath:/templates/products/list.html, the five standard expressions, th:text versus th:utext and XSS, th:each with #numbers and #temporals, a validated create form with th:field and th:errors, where BindingResult must go, Post/Redirect/Get with flash attributes, fragments, static CSS, what spring.thymeleaf.cache really changes, and HTML error pages.