Dependency Injection and Layers
Beans, services, repositories and constructor injection.
What you will learn
- Explain inversion of control
- Use @Service and constructor injection
- Structure controller/service/repository
The heart of Spring is inversion of control. Instead of a class creating the objects it depends on (new EmailSender()), it declares what it needs and Spring supplies it. This makes code easier to test (swap in a fake) and to change (replace an implementation without touching users of it).
Beans
A bean is an object created and managed by Spring's container. Mark a class with a stereotype annotation and component scanning registers it.
@Component: a generic bean.@Service: business logic.@Repository: data access.@RestController: web layer.@Configurationwith@Beanmethods: create beans manually, for example for third-party classes.
public interface NotificationSender {
void send(String to, String message);
}
@Service
class EmailSender implements NotificationSender {
public void send(String to, String message) {
System.out.println("Email to " + to + ": " + message);
}
}
@Service
public class OrderService {
private final NotificationSender sender; // depends on an interface
public OrderService(NotificationSender sender) { // Spring passes it in
this.sender = sender;
}
public void placeOrder(String email) {
// ...save order...
sender.send(email, "Order received");
}
}Prefer constructor injection: dependencies are explicit, can be final, and the class cannot exist in a half-built state. With a single constructor, @Autowired is unnecessary.
class OrderServiceTest {
@Test
void sendsConfirmation() {
var sent = new java.util.ArrayList<String>();
NotificationSender fake = (to, msg) -> sent.add(to + ":" + msg);
new OrderService(fake).placeOrder("a@b.com");
assertEquals(1, sent.size());
}
}The classic layers
- Controller: HTTP in and out; no business logic.
- Service: the rules of your application; transactions live here.
- Repository: talks to the database.
Dependencies point downward only: controllers call services, services call repositories. Keeping that direction makes each layer replaceable and testable.
Multiple implementations
If two beans implement the same interface, Spring cannot decide. Mark one @Primary, or select by name with @Qualifier("smsSender").
Bean scopes
Beans are singletons by default: one shared instance. So never keep per-request or per-user state in fields of a service.
If A needs B and B needs A, Spring fails at startup. It usually means the design needs a third class or an event.
Try it yourself
Create a PriceCalculator interface with a StandardCalculator and a DiscountCalculator, mark one @Primary, and inject it into an InvoiceService.
Show solution
interface PriceCalculator { double total(double subtotal); }
@Service @Primary
class DiscountCalculator implements PriceCalculator {
public double total(double s) { return s * 0.9; }
}
@Service
class StandardCalculator implements PriceCalculator {
public double total(double s) { return s; }
}
@Service
class InvoiceService {
private final PriceCalculator calc;
InvoiceService(PriceCalculator calc) { this.calc = calc; }
double invoice(double subtotal) { return calc.total(subtotal); }
}