Spring Boot Intermediate: Layers, Data and Testing
Services and Dependency Injection
In this lesson you will
- Explain beans and dependency injection
- Use constructor injection in layered classes
- Define a bean with @Configuration and @Bean
Introduction
As an application grows, putting everything in controllers becomes messy. Spring encourages layers, and its core idea, dependency injection, connects them. This lesson is written for Spring Boot 4.1.2, and the code was not compiled or run here.
Layers
HTTP request
|
Controller translates HTTP to Java calls, validates input, returns responses
|
Service business rules ("a member may borrow at most 5 books")
|
Repository reads and writes the databaseEach layer has one job, which makes code easier to read, change and test.
Beans
A bean is an object created and managed by Spring. Spring finds your classes marked with stereotype annotations during component scanning:
@Component: a generic bean@Service: business logic@Repository: data access@RestControllerand@Controller: web layer
Dependency injection
Instead of creating the objects you need with new, you declare them, and Spring supplies them.
Java
package com.example.demo.book;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class BookService {
private final BookStore store;
public BookService(BookStore store) {
this.store = store;
}
public List<Book> findAll() {
return store.findAll();
}
}Java
@RestController
@RequestMapping("/books")
public class BookController {
private final BookService service;
public BookController(BookService service) {
this.service = service;
}
@GetMapping
public List<Book> list() {
return service.findAll();
}
}Spring sees the controller needs a BookService, finds or creates that bean, and passes it to the constructor. With a single constructor, no @Autowired annotation is needed.
Why constructor injection
- Dependencies are required and visible.
- Fields can be final, so objects are immutable after creation.
- In tests, you can simply create the class with
new BookController(fakeService). No Spring needed. - It avoids hidden, half-initialised objects, which field injection with
@Autowiredcan cause.
Quick quiz
What is a benefit of constructor injection?
Programming to interfaces
Define an interface and let Spring inject whichever implementation exists:
Java
public interface BookStore {
List<Book> findAll();
}
@Repository
class InMemoryBookStore implements BookStore {
public List<Book> findAll() { return List.of(new Book(1, "Dune", 1965)); }
}Later you can replace the in-memory store with a database-backed one without changing the service. If several beans implement the same interface, Spring needs help choosing: mark one @Primary, or select with @Qualifier("name").
Defining beans yourself
For classes you do not own, such as a library object, create beans in a @Configuration class:
Java
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public Clock clock() {
return Clock.systemUTC();
}
}Now any class can ask for a Clock in its constructor. In tests you can provide a fixed clock, which makes time-dependent code easy to test.
Scope
By default, a bean is a singleton: one shared instance per application. That means beans must be thread-safe. Do not store per-request data in fields of a singleton bean.
Common problems
- "No qualifying bean": the class is not annotated, or is outside the scanned packages.
- Circular dependencies: A needs B and B needs A. Restructure, for example by moving shared logic to a third class.
- Business logic in controllers: move it to services.
Try it yourself
Refactor your BookController so it calls a BookService, which uses a BookStore interface with an in-memory implementation. Add a Clock bean, and use it to stamp the creation time of a book.
Summary
- Layers separate web handling, business rules and data access.
- Spring creates beans and injects them through constructors, so classes declare what they need.
- Prefer constructor injection and interfaces, define extra beans with @Configuration and @Bean, and keep singleton beans thread-safe.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading