Spring IoC Container
In the previous document, we covered what Spring is at a high level, including a brief mention of Inversion of Control (IoC). Now we'll dive into the mechanics that make Spring work. I consider Spring's entire philosophy to be built on four core concepts:
- IoC Container
- Beans
- Annotations
- Dependency Injection
It's difficult to explain one without implicitly referencing the others, which can make understanding the foundational aspects of the framework rather daunting for newcomers.
In this document, we'll try to unravel this interconnected web, one piece at a time.
Spring IoC Container
Imagine you're a chef. Instead of running to the farm for eggs and the mill for flour, you just shout "I need ingredients!" and they appear on your counter, ready to go. That's Spring's IoC Container, your personal assistant for Java objects.
It works like this:
- Component Scan: Spring scans your code for special classes marked with annotations like
@Component,@Service, or@Repository. - Bean Creation: It creates instances of these classes, called "beans," and manages their entire lifecycle.
- Dependency Injection: When one of your beans needs another one, Spring automatically provides it.
Beans
A bean is an object managed by the Spring IoC (Inversion of Control) container.
- Beans are defined in the Spring configuration, either via:
- Annotations (e.g.,
@Component,@Service,@Repository,@Configuration). - XML configuration (do NOT in modern Spring).
- Java-based configuration (
@Beanmethods in@Configurationclasses).
- Annotations (e.g.,
- Singleton by default: By default, a Spring bean is a singleton (one instance per container). This can be customized with scopes like
@Scope("prototype"). - Beans can be injected into each other. This promotes loose coupling and testability.
- The container controls a bean’s lifecycle, from instantiation to destruction. You can define custom hooks with annotations like
@PostConstructand@PreDestroy.
Annotations
Annotations are Spring’s way of letting you tag your code with instructions like "Hey Spring, manage this class!" or "Inject that dependency here!".
Bean Definition
| Annotation | Meaning | Use Case |
|---|---|---|
@Component | "Spring, manage this class!" | Generic beans |
@Service | "Business logic here!" | Service-layer classes |
@Repository | "Database interactions here!" | DAOs/DB classes (adds exception translation) |
@RestController | "API endpoint!" (Combines @Controller + @ResponseBody) | REST APIs |
Dependency Injection
| Annotation | Meaning | Example |
|---|---|---|
@Autowired | "Inject a bean here!" | Constructor/field/setter |
@Primary | "Choose me first!" | Resolve ambiguous bean conflicts |
@Qualifier | "Inject THIS specific bean" | @Qualifier("mysqlDb") |
Configuration
| Annotation | Meaning | Example |
|---|---|---|
@Configuration | "This class configures beans!" | Setup database/3rd-party libs |
@Bean | "Here’s a bean to manage!" | Methods returning complex objects |
@Value | "Inject a property value!" | @Value("${api.key}") |
Web/REST
| Annotation | Meaning | Example |
|---|---|---|
@RequestMapping | "Map requests to this method" | @RequestMapping("/users") |
@GetMapping | "Handle GET requests" | @GetMapping("/{id}") |
@PostMapping | "Handle POST requests" | @PostMapping("/create") |
@RequestBody | "Convert JSON → Java object" | createUser(@RequestBody User user) |
@PathVariable | "Get URL parameters" | @PathVariable Long id |
Lombok
| Annotation | Purpose | Example |
|---|---|---|
@Getter / @Setter | Auto-generate getters/setters | @Getter @Setter private String username; |
@ToString | Auto-generate toString() | @ToString(exclude = "password") |
@EqualsAndHashCode | Auto-generate equals() and hashCode() | @EqualsAndHashCode(callSuper = true) |
@NoArgsConstructor | Generate no-arg constructor | @NoArgsConstructor |
@AllArgsConstructor | Generate constructor with all args | @AllArgsConstructor |
@RequiredArgsConstructor | Generate constructor with final/@NonNull fields | @RequiredArgsConstructor |
@Data | All-in-one (@Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor) | @Data public class User { ... } |
@Builder | Implement Builder pattern | User.builder().name("Alice").build(); |
@Slf4j | Inject logger (Logger log) | log.info("User created: {}", username); |
If you're using Groovy, you don't need Lombok. Groovy’s built-in AST (Abstract Syntax Tree) transformations do everything Lombok does, but they do it as a first-class citizen of the language rather than a "hack" that plugs into the compiler.
| Feature | Lombok | Groovy AST |
|---|---|---|
| The "Everything" Bagel | @Data | @Canonical (Combines Equals, HashCode, ToString, etc.) |
| Immutability | @Value | @Immutable |
| Constructors | @AllArgsConstructor | @TupleConstructor |
| Builder Pattern | @Builder | @Builder |
| Logging | @Slf4j | @Slf4j (or @Log) |
| Delegation | @Delegate (Experimental-ish) | @Delegate (Solid and powerful) |
| Lazy Loading | @Getter(lazy=true) | @Lazy |
Kotlin was designed from the ground up to solve the problems Lombok addresses, but it does so as part of the language syntax rather than relying on annotation processing tricks. This means more readable, idiomatic code with less "magic" happening behind the scenes.
| Lombok Feature | Kotlin Equivalent | Why it’s better |
|---|---|---|
@Data / @Value | data class | You get equals, hashCode, toString, and copy() in one line. |
@Getter / @Setter | Properties (val/var) | Accessors are built-in. user.name calls the getter under the hood. |
@AllArgsConstructor | Primary Constructor | Defined right in the class header. No magic required. |
@Builder | Named & Default Params | User(name = "Franco", age = 30) is cleaner than a builder pattern. |
@Slf4j | Companion object or Libs | Most use private val log = LoggerFactory.getLogger(javaClass) or a library like kotlin-logging. |
@NonNull | Nullable Types (String?) | Kotlin’s type system handles null safety at compile time, instead of relying solely on runtime checks. |
Tips
-
Prefer constructor injection (
private final+ Lombok@RequiredArgsConstructor) over field injection (@Autowired). -
Layer your annotations.
- Java
- Kotlin
- Groovy
@Repository // Data layerpublic class UserRepository { ... }@Service // Business logicpublic class UserService { ... }@RestController // API layerpublic class UserController { ... }@Repository // Data layerclass UserRepository { ... }@Service // Business logicclass UserService { ... }@RestController // API layerclass UserController { ... }@Repository // Data layerclass UserRepository { ... }@Service // Business logicclass UserService { ... }@RestController // API layerclass UserController { ... } -
Use
@Configuration+@Beanto wire complex dependencies.- Java
- Kotlin
- Groovy
@Configurationpublic class SecurityConfig {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}}@Configurationclass SecurityConfig {@Beanfun passwordEncoder(): PasswordEncoder {return BCryptPasswordEncoder()}}@Configurationclass SecurityConfig {@BeanPasswordEncoder passwordEncoder() {new BCryptPasswordEncoder()}} -
Environment-specific beans: Use
@Profilefor dev/staging/prod setups.- Java
- Kotlin
- Groovy
@Profile("dev")@Servicepublic class MockPaymentService implements PaymentService { ... }@Profile("dev")@Serviceclass MockPaymentService : PaymentService { ... }@Profile("dev")@Serviceclass MockPaymentService implements PaymentService { ... } -
Avoid annotation soup: Stick to one role-specific annotation.
- Java
- Kotlin
- Groovy
@Component @Service // Redundant!public class UserService { ... }@Component @Service // Redundant!class UserService { ... }@Component @Service // Redundant!class UserService { ... }
Dependency Injection
You tell Spring what you need by asking for the interface. There are a few ways to do it, but only one right way.
Constructor Injection (The Right Way)
- Java
- Kotlin
- Groovy
@RestController
public class CheckoutController {
private final PaymentService paymentService;
// You ask for the "what" (the interface) in your constructor
public CheckoutController(PaymentService paymentService) {
this.paymentService = paymentService;
}
//Controller logic...
}
@RestController
class CheckoutController(private val paymentService: PaymentService) {
// In Kotlin, the primary constructor is concise and does the same thing.
//Controller logic...
}
@RestController
class CheckoutController {
private final PaymentService paymentService
// You ask for the "what" (the interface) in your constructor
CheckoutController(PaymentService paymentService) {
this.paymentService = paymentService
}
//Controller logic...
}
This is the gold standard. Why?
- No Nulls: The object can't even be created without its dependencies. It forces you to have a valid, ready-to-use object from the start.
- Immutability: By declaring the field
final, you guarantee it can't be changed later. You won't accidentally swap your database for a toaster mid-request. - Testing-Friendly: In your unit tests, you don't need Spring. You just call
new CheckoutController(new MockPaymentService())and you're done. Clean, simple, and fast.
Even better, you can get rid of the boilerplate. In Java, you'd use a library like Lombok, but in Kotlin this is a built-in language feature!
- Java
- Kotlin
- Groovy
@RestController
@RequiredArgsConstructor // <-- Lombok annotation
public class CheckoutController {
private final PaymentService paymentService;
//Controller logic
}
Lombok generates the constructor for all your final fields automatically.
Clean, concise, and impossible to get wrong. This is the way.
@RestController
// In Kotlin, this is built into the language with primary constructors.
class CheckoutController(private val paymentService: PaymentService) {
//Controller logic
}
In Kotlin, you don't need a special annotation. Declaring a property in the
primary constructor (class ... (...)) is all you need to do.
@RestController
@Immutable // This generates a constructor for all properties
class CheckoutController {
PaymentService paymentService
//Controller logic
}
Groovy's AST (Abstract Syntax Tree) transformations can do the same work as
Lombok. @Immutable is one of many that can generate constructors and more.
Setter Injection
This is for optional dependencies. The problem is, your object can exist in a state where its dependencies are null. It's less safe and makes your code harder to reason about.
- Java
- Kotlin
- Groovy
@RestController
public class CheckoutController {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
//...
}
@RestController
class CheckoutController {
@Autowired
lateinit var paymentService: PaymentService
//...
}
@RestController
class CheckoutController {
PaymentService paymentService
@Autowired
void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService
}
//...
}
Field Injection
It looks clean, but it's a trap. It hides the dependencies, makes your class harder to test without Spring's magic, and makes it impossible to create immutable objects.
- Java
- Kotlin
- Groovy
@RestController
public class CheckoutController {
@Autowired
private PaymentService paymentService;
//...
}
@RestController
class CheckoutController {
@Autowired
private lateinit var paymentService: PaymentService
//...
}
@RestController
class CheckoutController {
@Autowired
private PaymentService paymentService
//...
}
As a final note on this topic, I really recommend CodeAesthetic’s video “Dependency Injection, The Best Pattern”. It drives the point home beautifully.