Learn / Frameworks / Spring Boot / Dependency Injection and Layers

Intermediate 14 min

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.
  • @Configuration with @Bean methods: 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.

Circular dependencies

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); }
}