Let Spring create and wire your objects. Declare beans with stereotype annotations, inject them through the constructor, and control scope and ambiguity.
Why: "Inversion of Control" means you do not create your objects with new — you declare them as beans and Spring’s container builds them, wires their dependencies, and hands them out. Note: this is what makes code testable — you swap a real bean for a fake without touching the class that uses it.
// Without Spring — you wire everything by hand:
var repo = new BookRepository();
var service = new BookService(repo); // you manage the graph
var controller = new BookController(service);
// With Spring — you declare beans and the container wires them for you.Why: stereotype annotations register a class as a bean during component scanning. When @Service: business logic. When @Repository: data access (it also translates persistence exceptions). When @Component: anything else. They are the same mechanism with different intent labels.
import org.springframework.stereotype.Service;
@Service
public class BookService {
public String greet() {
return "Books!";
}
}Why: declare what you need as constructor parameters and Spring passes the matching beans in. When constructor (not @Autowired on a field): it makes dependencies final and explicit, and the class stays a plain object you can unit-test with new. Note: a single constructor needs no @Autowired.
@RestController
public class BookController {
private final BookService service; // final = required, immutable
public BookController(BookService service) { // Spring injects it
this.service = service;
}
@GetMapping("/books/greeting")
public String greeting() {
return service.greet();
}
}When @Bean: you need a bean from a class you do not own (a library type) so you cannot annotate it. Where: a method inside a @Configuration class — its return value becomes a bean, and its own parameters are injected too.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public RestClient restClient() { // a library type you can't annotate
return RestClient.create();
}
}Note: by default every bean is a singleton — one shared instance for the whole application. When prototype: a new instance each time it is injected or requested. When request/session: web scopes, one per HTTP request or user session. Stick with singleton unless a bean holds per-request state.
import org.springframework.context.annotation.Scope;
@Service
@Scope("singleton") // the default — one shared instance
public class CatalogService { }
@Service
@Scope("prototype") // a fresh instance every time it's needed
public class ShoppingCart { }When two beans fit one type, Spring cannot choose. When @Primary: marks the default winner. When @Qualifier: names exactly which bean to inject at a specific spot. Note: @Qualifier on the injection point overrides @Primary.
public interface PaymentGateway { }
@Service @Primary
class StripeGateway implements PaymentGateway { } // default choice
@Service @Qualifier("paypal")
class PaypalGateway implements PaymentGateway { }
@Service
class CheckoutService {
CheckoutService(@Qualifier("paypal") PaymentGateway gateway) {
// gets PaypalGateway, overriding @Primary
}
}