CodeGym /Courses /Module 5. Spring /Spring Architecture: IoC, DI, Spring Beans

Spring Architecture: IoC, DI, Spring Beans

Module 5. Spring
Level 1 , Lesson 1
Available

Today we'll get to know the three pillars Spring is built on: IoC (Inversion of Control), DI (Dependency Injection), and Spring Beans. It all sounds like magic, but in practice — these are a Java developer's best friends.

1. Inversion of Control (IoC)

Let's recall good old object-oriented development. In object-oriented programming you often run into situations where one object depends on another. Usually the developer creates the dependent object right in the code:


public class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }
}

It seems to work, but there's a problem: you've tightly coupled Car to Engine. If you need to plug in a different engine type, like ElectricEngine, you'd have to open up Car's code and change it. Not very convenient, and it's easy to break something accidentally! That change breaks the flexibility and maintainability of the code.

And that's where IoC (Inversion of Control) comes in. The idea is simple: instead of the object creating its own dependency (Engine in our case), the responsibility for that is handed over to a container.

The IoC container in Spring

Spring provides an IoC container that creates, configures, and manages the lifecycle of objects. All you need to do is describe the dependencies (we'll show how). This removes tight coupling and lets you focus on business logic instead of object creation.

Example:


public class Car {
    private Engine engine;

    // Dependency is passed through the constructor
    public Car(Engine engine) {
        this.engine = engine;
    }
}

Now Car doesn't know which exact Engine it will get. The IoC container handles that and saves us a headache. The actual IoC container isn't shown in this code — it's just a class that's ready to be used with an IoC container.

2. Dependency Injection (DI): the key to managing dependencies

Dependency Injection (DI) is the way IoC is implemented. The Spring container injects dependencies (for example, Engine) into the required object (Car) using one of the following approaches:

  1. Through the constructor.
  2. Through a setter (method).
  3. Through a field (annotation).

In practice DI feels like magic. For example, like this:


@Component
public class Engine {
    // Engine class
}

@Component
public class Car {
    private Engine engine;

    // Spring will automatically inject the dependency via the constructor
    @Autowired
    public Car(Engine engine) {
        this.engine = engine;
    }
}

Spring sees that Car depends on Engine, and automatically creates an Engine, passing it into Car. You don't have to worry about creating or passing objects yourself!


3. Spring Beans: building the magic in a controlled way

Spring Bean is an object managed by the IoC container. The container creates beans, configures them, wires their dependencies, and manages their lifecycle. If you need an object, just ask the container: "Hey, give me a bean!", and it will do it for you.

You can turn a class into a bean by marking it with special annotations:

  • @Component — a general-purpose annotation for creating beans.
  • @Service — marks beans that implement business logic.
  • @Repository — for beans that work with data (like JPA repositories).
  • @Controller — for beans that handle HTTP requests (in the context of Spring MVC).

Example of creating a bean


@Component
public class Engine {
    // This is a Spring bean. Now the container will manage this object.
}

@Component
public class Car {
    private final Engine engine;

    @Autowired // Tell Spring to automatically inject the dependency
    public Car(Engine engine) {
        this.engine = engine;
    }
}

4. Ways of injecting dependencies

Now let's break down the three main ways to inject dependencies using DI.

  1. Constructor injection

This approach is preferred because it makes dependencies obvious and immutable. Here's an example:


@Component
public class Car {
    private final Engine engine;

    @Autowired
    public Car(Engine engine) {
        this.engine = engine;
    }
}
  1. Setter injection

The plus of this approach is flexibility. You can replace the dependency after the object is created. The downside is it can lead to mutable state, which isn't always great.


@Component
public class Car {
    private Engine engine;

    @Autowired
    public void setEngine(Engine engine) {
        this.engine = engine;
    }
}
  1. Field injection (annotation)

The most controversial way, because it breaks encapsulation. But for simple cases it's a viable option.


@Component
public class Car {
    @Autowired
    private Engine engine;
}

5. Lifecycle of Spring Beans

  1. Creation — the container creates the object.
  2. Initialization — the container injects dependencies and calls methods like @PostConstruct.
  3. Usage — the bean is ready to work.
  4. Destruction — the container shuts down the bean and calls methods like @PreDestroy.

Methods to manage the lifecycle:

  • @PostConstruct — method called after bean creation and completion of DI.
  • @PreDestroy — method called before the bean is destroyed.

Code example:


@Component
public class Car {

    @PostConstruct
    public void init() {
        System.out.println("Car is ready to drive!");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("Car is being destroyed...");
    }
}

6. Common mistakes when working with IoC and DI

Don't worry, mistakes happen to everyone. Here's a classic case: you forgot to annotate the Engine class with @Component. Because of that Spring can't find the bean:


Error creating bean with name 'car': Unsatisfied dependency expressed through constructor

Tip: check that all dependencies are "included" in the context, meaning they have annotations like @Component.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION