Skip to main content

Spring IoC Container

I consider Spring's entire philosophy to be built on four core concepts:

  • IoC Container
  • Beans
  • Annotations
  • Dependency Injection
Scroll to zoom • Drag corner to resize

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.

Meme 7e85e1970f1b2ae65f0ecf478400730e

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:

  1. Component Scan: Spring scans your code for special classes marked with annotations like @Component, @Service, or @Repository.
  2. Bean Creation: It creates instances of these classes, called "beans," and manages their entire lifecycle.
  3. 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 (@Bean methods in @Configuration classes).
  • 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 @PostConstruct and @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

AnnotationMeaningUse 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

AnnotationMeaningExample
@Autowired"Inject a bean here!"Constructor/field/setter
@Primary"Choose me first!"Resolve ambiguous bean conflicts
@Qualifier"Inject THIS specific bean"@Qualifier("mysqlDb")

Configuration

AnnotationMeaningExample
@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

AnnotationMeaningExample
@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

AnnotationPurposeExample
@Getter / @SetterAuto-generate getters/setters@Getter @Setter private String username;
@ToStringAuto-generate toString()@ToString(exclude = "password")
@EqualsAndHashCodeAuto-generate equals() and hashCode()@EqualsAndHashCode(callSuper = true)
@NoArgsConstructorGenerate no-arg constructor@NoArgsConstructor
@AllArgsConstructorGenerate constructor with all args@AllArgsConstructor
@RequiredArgsConstructorGenerate constructor with final/@NonNull fields@RequiredArgsConstructor
@DataAll-in-one (@Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor)@Data public class User { ... }
@BuilderImplement Builder patternUser.builder().name("Alice").build();
@Slf4jInject logger (Logger log)log.info("User created: {}", username);
Not for Groovy Projects!

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.

FeatureLombokGroovy 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
Not for Kotlin Projects!

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 FeatureKotlin EquivalentWhy it’s better
@Data / @Valuedata classYou get equals, hashCode, toString, and copy() in one line.
@Getter / @SetterProperties (val/var)Accessors are built-in. user.name calls the getter under the hood.
@AllArgsConstructorPrimary ConstructorDefined right in the class header. No magic required.
@BuilderNamed & Default ParamsUser(name = "Franco", age = 30) is cleaner than a builder pattern.
@Slf4jCompanion object or LibsMost use private val log = LoggerFactory.getLogger(javaClass) or a library like kotlin-logging.
@NonNullNullable Types (String?)Kotlin’s type system handles null safety at compile time, instead of relying solely on runtime checks.

Tips

  1. Prefer constructor injection (private final + Lombok @RequiredArgsConstructor) over field injection (@Autowired).

  2. Layer your annotations.

    @Repository  // Data layer
    public class UserRepository { ... }

    @Service // Business logic
    public class UserService { ... }

    @RestController // API layer
    public class UserController { ... }
  3. Use @Configuration + @Bean to wire complex dependencies.

    @Configuration
    public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
    }
    }
  4. Environment-specific beans: Use @Profile for dev/staging/prod setups.

    @Profile("dev")
    @Service
    public class MockPaymentService implements PaymentService { ... }
  5. Avoid annotation soup: Stick to one role-specific annotation.

    @Component @Service  // Redundant!
    public 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)

@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...
}

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!

@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.

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.

@RestController
public class CheckoutController {

private PaymentService paymentService;

@Autowired
public 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.

@RestController
public 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.