1. The IoC Container & Beans
In ordinary Java, an object that needs a collaborator usually creates it directly —
new OrderRepository() inside OrderService, for example.
That means OrderService owns the decision of exactly which
implementation it depends on and exactly when it's constructed.
Inversion of Control (IoC) flips that: instead of your class
constructing its own dependencies, something external constructs them and hands
them to your class. That "something external" is the IoC container,
and in Spring it's represented at runtime by an ApplicationContext.
A bean is simply an object whose creation and lifecycle the
container manages, instead of you managing it with new. When
SpringApplication.run() executes, Spring builds an
ApplicationContext, figures out which classes should become beans,
works out the order they need to be created in based on what depends on what, and
wires the whole graph together — all before your first log line prints.
package com.codeverse.week02;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String greet(String name) {
return "Hello, " + name + "!";
}
}
package com.codeverse.week02;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private final GreetingService greetingService;
public GreetingController(GreetingService greetingService) {
this.greetingService = greetingService;
}
@GetMapping("/greet")
String greet(@RequestParam(defaultValue = "world") String name) {
return greetingService.greet(name);
}
}
Nothing in this code ever calls new GreetingService() or
new GreetingController(...). At startup, Spring sees
@Service on GreetingService and registers it as a bean.
It sees @RestController on GreetingController, notices its
constructor asks for a GreetingService, finds the
GreetingService bean it already created, and passes it in. That
resolution — "this bean needs that bean, so build that one first" — is the container
doing dependency injection for you.
2. Bean Lifecycle & Scopes
A bean's life has more stages than "constructed, then used." The container instantiates it, injects its dependencies, runs any initialization callbacks, hands it out whenever something needs it, and — for beans it owns until the end — runs cleanup callbacks when the application context shuts down.
package com.codeverse.week02;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class ConnectionPoolManager {
@PostConstruct
void warmUp() {
// Runs once, right after Spring has injected every dependency
// this bean needed -- a safe place to open connections, prime
// caches, or validate configuration.
System.out.println("ConnectionPoolManager: pool warmed up");
}
@PreDestroy
void shutdown() {
// Runs once, as the ApplicationContext closes -- release
// resources cleanly instead of leaking them.
System.out.println("ConnectionPoolManager: pool drained");
}
}
@PostConstruct fires after the constructor and all dependency injection
are complete, so it's the right place for setup logic that depends on those injected
collaborators being present. @PreDestroy fires as the context shuts
down, useful for closing connections or flushing buffers.
Singleton vs. prototype scope
By default every bean is a singleton — the container creates exactly one instance and hands out that same instance everywhere it's needed. This is almost always what you want for services, repositories, and controllers, which hold no per-request mutable state of their own.
A prototype-scoped bean gets a brand-new instance every time it's requested from the container:
package com.codeverse.week02;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ReportBuilder {
// Accumulates mutable state across several method calls while one
// report is being assembled -- sharing a singleton instance of this
// across concurrent requests would corrupt that state.
private final StringBuilder buffer = new StringBuilder();
public ReportBuilder append(String line) {
buffer.append(line).append(System.lineSeparator());
return this;
}
public String build() {
return buffer.toString();
}
}
You'd reach for prototype scope specifically when a bean accumulates
mutable, per-use state that must not be shared across callers — a stateful builder
like the one above, for instance. It's the exception, not the default: reach for it
only when you notice a bean actually needs fresh state per use, not preemptively.
3. Dependency Injection Styles
Spring supports three ways to get a dependency into a bean. All three work. Only one of them should be your default.
Field injection — avoid
@Service
public class OrderService {
@Autowired
private PaymentClient paymentClient; // injected by reflection, after construction
}
Setter injection — for optional dependencies only
@Service
public class OrderService {
private NotificationClient notificationClient;
@Autowired(required = false)
public void setNotificationClient(NotificationClient notificationClient) {
this.notificationClient = notificationClient;
}
}
Constructor injection — the default
@Service
public class OrderService {
private final PaymentClient paymentClient;
private final InventoryClient inventoryClient;
// With a single constructor, @Autowired is optional -- Spring uses
// it automatically. Keeping it explicit is still common practice.
public OrderService(PaymentClient paymentClient, InventoryClient inventoryClient) {
this.paymentClient = paymentClient;
this.inventoryClient = inventoryClient;
}
}
Constructor injection wins on every axis that matters in practice:
- Immutability. Fields can be
final, so onceOrderServiceis built, its dependencies can never silently change out from under it. - Testability without reflection. A unit test just calls
new OrderService(fakePaymentClient, fakeInventoryClient)— no Spring context, no reflection tricks, no test-only annotations required to populate a private field. - Fail fast at startup. If a required dependency's bean doesn't
exist, the app refuses to start with a clear error, instead of deploying successfully
and throwing a
NullPointerExceptionthe first time a request happens to hit the missing field. - Honesty about required vs. optional. A long constructor parameter list is a visible signal that a class is doing too much — field injection hides that signal because dependencies are scattered across the class body instead of declared in one place.
Week 8's testing module leans on exactly the "no reflection required" point above. Every service you write with constructor injection this week can be unit-tested with a plain new call and hand-built fakes — no @MockBean, no Spring context startup, no slow tests.
4. Component Scanning & Stereotypes
Spring doesn't scan your entire classpath looking for beans — it needs to be told which classes are candidates. The stereotype annotations are how you mark a class as one:
@Component— the generic marker; "this class is a bean."@Service— a@Componentspecialization for business logic; purely a naming convention that also documents intent.@Repository— a@Componentspecialization for data access; additionally enables automatic translation of database exceptions into Spring's unifiedDataAccessExceptionhierarchy.@Controller/@RestController— a@Componentspecialization for handling HTTP requests, which you already used in Week 1.
@SpringBootApplication is itself a combination of three annotations,
one of which is @ComponentScan. With no arguments,
@ComponentScan scans the package containing the annotated class and
every sub-package beneath it:
com.codeverse.week02
├── Week02Application.java ← @SpringBootApplication lives here
├── GreetingController.java ← found: same package
├── service/
│ ├── GreetingService.java ← found: sub-package
│ └── impl/
│ └── FriendlyGreetingService.java ← found: sub-sub-package
└── other/
└── unrelated/
└── SomeClass.java ← NOT found if this were outside
com.codeverse.week02 entirely
This is why every generated Spring Boot project puts its
@SpringBootApplication class at the root package — one
level above everything else. Put it in a leaf package by mistake, and any
@Service or @Repository class outside that package's
subtree simply never gets registered, with no error — the bean just silently doesn't
exist, and you find out when injection fails.
5. @Configuration Classes & @Bean Methods
Stereotype annotations only work on classes you own and can edit. For a class from a
third-party library — or anything that needs to be built with logic rather than a
bare constructor call — you register it explicitly with a @Configuration
class and a @Bean-annotated method instead.
package com.codeverse.week02;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
// ObjectMapper is a third-party class -- you can't add @Component
// to it. This is the standard way to register a bean for something
// you don't own, built with whatever setup logic it needs.
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
The rule of thumb: reach for a stereotype annotation on your own classes by default;
reach for an explicit @Bean method when you're wiring up a class you
don't control, when construction needs conditional logic, or when you need more than
one bean of the same type configured differently (the second implementation you'll
add in this week's exercise is a good example of the latter).
6. Auto-Configuration & Starters Explained
In Week 1, adding a single dependency — spring-boot-starter-web — to
pom.xml was enough for an embedded Tomcat server to appear and start
handling HTTP requests, with zero configuration written by you. That's
auto-configuration, the third piece bundled inside
@SpringBootApplication alongside component scanning.
A starter like spring-boot-starter-web is not magic —
it's just a Maven dependency that pulls in a curated set of libraries (Tomcat,
Spring MVC, Jackson for JSON) plus an auto-configuration module for each. Each
auto-configuration class is guarded by conditional annotations that decide whether it
should activate at all:
@Configuration
@ConditionalOnClass(Tomcat.class)
public class TomcatWebServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean(ServletWebServerFactory.class)
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
@ConditionalOnClass(Tomcat.class) means "only register this
configuration if the Tomcat class is actually present on the classpath" — which it
is, because spring-boot-starter-web pulled it in as a transitive
dependency. Remove that starter, and this whole auto-configuration class quietly
never activates; no Tomcat classes, no Tomcat bean.
@ConditionalOnMissingBean(ServletWebServerFactory.class) means "only
register Spring's default Tomcat factory if you haven't already defined your
own bean of that type" — so any @Bean method you write yourself always
takes priority over Spring Boot's defaults, letting you override selectively instead
of all-or-nothing.
You can see exactly which auto-configuration classes activated — and, just as
usefully, which ones Spring considered and rejected, and why — by running your app
with the --debug flag:
./mvnw spring-boot:run -Dspring-boot.run.arguments=--debug
# Look for the "CONDITIONS EVALUATION REPORT" near the top of the log:
#
# Positive matches:
# -----------------
# DispatcherServletAutoConfiguration matched:
# - @ConditionalOnClass found required class 'jakarta.servlet.Servlet' ...
#
# Negative matches:
# -----------------
# DataSourceAutoConfiguration:
# Did not match:
# - @ConditionalOnClass did not find required class
# 'javax.sql.DataSource' ...
That single spring-boot-starter-web dependency you added in Week 1 is the entire reason Tomcat, Spring MVC and Jackson auto-configured themselves. When Week 3 has you add spring-boot-starter-validation or a database starter, the same mechanism — starter on the classpath, matching @ConditionalOnClass auto-configuration activates — is what wires the new capability in.
Understanding that a controller is "just a bean, injected like any other" is what makes Week 3's REST controllers feel unsurprising rather than magical — the routing annotations change, but the constructor-injection pattern you practice this week doesn't.
7. Hands-on Exercise
Layer Week 1's Greeting service behind an interface
Take the greeting endpoint you built in Week 1 and turn it into a small, properly-layered example of everything covered this week: an interface-backed bean, constructor injection, two competing implementations, and a lifecycle callback you can watch fire.
Requirements:
- Define a
GreetingServiceinterface with one method,String greet(String name). - Create a
FriendlyGreetingServiceimplementation annotated@Serviceand@Primary, returning something like"Hey there, " + name + "!". - Create a second implementation,
FormalGreetingService, annotated@Service("formalGreetingService"), returning"Good day, " + name + "."— do not mark it@Primary. - Update
GreetingControllerto depend on theGreetingServiceinterface (not a concrete class) via constructor injection. Add a second constructor parameter using@Qualifier("formalGreetingService")to also inject the formal variant, and expose it on a second endpoint, e.g.GET /greet/formal, so both implementations are demonstrably wired into the same controller at once. - Add a
@PostConstructmethod toFriendlyGreetingServicethat logs a line proving the bean was constructed (e.g."FriendlyGreetingService bean created"), and confirm it prints once at startup — not once per request.
If you inject a bare GreetingService parameter with no @Qualifier and both implementations exist, Spring uses whichever one is marked @Primary to resolve the ambiguity. @Qualifier("beanName") is how you deliberately ask for the non-primary one instead — you'll need both approaches in the same controller for this exercise.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why is constructor injection preferred over field injection (@Autowired on a private field) in idiomatic Spring code?
Why is constructor injection preferred over field injection (@Autowired on a private field) in idiomatic Spring code?
Constructor injection lets dependency fields be declared final, so once the object exists its collaborators can never be silently swapped or left null after construction. It also means unit tests can build the class with a plain new call and hand-written fakes, with no Spring container or reflection involved, which keeps tests fast and simple. And because the constructor demands every required dependency up front, a missing bean causes the application to fail immediately at startup with a clear error, rather than deploying successfully and throwing a NullPointerException the first time a request happens to touch the unset field. Field injection hides all of these problems: it works via reflection after construction, so an object can exist in a half-wired, untestable state, and tests need a running Spring context or special test hooks just to populate a private field.
Q2
What does @ConditionalOnClass do in an auto-configuration class, and why does it matter for how starters work?
What does @ConditionalOnClass do in an auto-configuration class, and why does it matter for how starters work?
@ConditionalOnClass tells Spring Boot to only register a particular auto-configuration class if a specified class is actually present on the classpath at runtime. This is what makes starters work as an all-or-nothing bundle: spring-boot-starter-web pulls in the Tomcat and Spring MVC jars as transitive dependencies, and because those classes are now on the classpath, the matching auto-configuration classes (guarded by @ConditionalOnClass(Tomcat.class) and similar) activate automatically and register beans like the embedded servlet container. If that starter were removed, those classes would no longer be present, the condition would fail, and the auto-configuration would silently skip itself instead of registering beans for a library that isn't even on the classpath — no error, just quietly not wiring in a capability you didn't ask for.
Q3
What's the practical difference between singleton and prototype bean scope, and when would you actually reach for prototype?
What's the practical difference between singleton and prototype bean scope, and when would you actually reach for prototype?
A singleton bean, the default, is created once by the container and that same instance is handed out every time it's needed anywhere in the application — appropriate for the vast majority of beans, like services and repositories, that hold no per-use mutable state of their own. A prototype-scoped bean gets a brand-new instance constructed every single time it's requested from the container, so no state leaks between different callers. You'd reach for prototype specifically when a bean accumulates mutable state across a sequence of method calls that must not be shared or corrupted by concurrent use, such as a stateful builder object being filled in over several steps. Reaching for prototype scope by default, rather than only when a bean genuinely needs fresh per-use state, is a common mistake — most beans should stay singleton.
Q4
Why does it matter which package your @SpringBootApplication class lives in, relative to your @Service and @Repository classes?
Why does it matter which package your @SpringBootApplication class lives in, relative to your @Service and @Repository classes?
@SpringBootApplication includes @ComponentScan, which by default only scans the package containing the annotated class and every sub-package beneath it — it does not scan sibling packages or anything above it. That's why every generated Spring Boot project places its application class at the root package, one level above every other package in the project: it guarantees every @Service, @Repository, and @Component class anywhere in the codebase sits inside that scanned subtree. If the application class were moved into a leaf package instead, any stereotype-annotated class outside that leaf's subtree would simply never be registered as a bean, with no error raised — the failure would only surface later as a confusing "no qualifying bean" exception wherever something tried to inject it.