← Back to the section

This is the first topic in Spring and the foundation for everything else: why you need a container, how it creates and wires objects, and where the pitfalls are.

the container assembles the bean before anyone asks for it PaymentClient dependency PaymentClientcreated first OrderService constructor OrderServicepayment already here @PostConstruct fields are set @PostConstructfields are set proxy @Transactional proxy@Transactional ready kept in the container readysingleton — one for all thread 1 thread 2 shutdown → @PreDestroy (singletons only)

The assembly order is fixed: first the dependency, then the constructor it is passed into, then @PostConstruct — by that moment every field is filled in. The finished bean is one: thread 1 and thread 2 get the same instance.

What IoC and DI are in plain terms

Normally an object creates what it needs by itself:

class OrderService {
    private final PaymentClient payment = new PaymentClient(); // we create it ourselves
}

The problem: OrderService is hard-wired to a specific PaymentClient — you can't swap it in a test or configure it from outside.

Inversion of Control is the idea "don't create your dependencies yourself, let someone outside provide them": object creation moves out of your code into the container.

Dependency Injection is the concrete technique Spring uses to implement IoC: the container creates the PaymentClient itself and passes it into OrderService.

@Service
class OrderService {
    private final PaymentClient payment;
    OrderService(PaymentClient payment) { this.payment = payment; } // it was given to us
}

Short formula: IoC is the principle, DI is the pattern that implements it.

ApplicationContext and BeanFactory

The Spring container comes in two flavors:

  • BeanFactory — the basic container: reads object descriptions and creates them on demand. You almost never use it directly.
  • ApplicationContext — a BeanFactory plus everything a real application needs: events, properties (Environment), localization messages, resource loading. It is what SpringApplication.run(App.class, args) returns on startup.

The objects the container manages are called beans. On startup Spring scans packages, finds classes annotated with @Component (and its special cases @Service, @Repository, @Controller), creates beans from them, and wires them together.

Injection styles and why constructor injection is preferred

You can inject a dependency in three ways:

// 1. Via the constructor — recommended
@Service
class OrderService {
    private final PaymentClient payment;
    OrderService(PaymentClient payment) { this.payment = payment; }
}

// 2. Via a field — concise, but problematic
@Service
class OrderService {
    @Autowired private PaymentClient payment;
}

// 3. Via a setter — for optional dependencies
@Service
class OrderService {
    private PaymentClient payment;
    @Autowired void setPayment(PaymentClient p) { this.payment = p; }
}

Why the constructor is usually the choice:

  • the field can be made final — the object can't be left "half-built";
  • all dependencies are visible at once: ten parameters in the constructor is a signal the class does too much;
  • it's easy to test — the object is created with a plain new and mocks, without starting Spring;
  • circular dependencies surface immediately instead of hiding (more on this below).

With a single constructor @Autowired isn't needed — Spring uses it anyway.

Scopes — how long a bean lives

Scope determines how many instances of a bean the container creates. There are six built-in ones; two matter in practice:

  • singleton (default) — one instance for the entire container. Almost all beans live this way: @Service, @Repository, @Controller. That's why beans are made without mutable state (stateless) — a single instance serves all requests in parallel.
  • prototype — a new instance on every request for the bean. Spring creates it and "lets it go": @PreDestroy is not called for a prototype.

Web applications add request (a new bean per HTTP request), session (per user session), application, and websocket.

How to inject a prototype into a singleton

A common trap: injected plainly, a prototype bean lands in the singleton once, at its creation, and the "freshness" is lost. A fresh instance on every call is what ObjectProvider gives you:

@Service
class OrderService {
    private final ObjectProvider<RequestContext> provider; // RequestContext is a prototype
    OrderService(ObjectProvider<RequestContext> p) { this.provider = p; }

    void process() {
        RequestContext ctx = provider.getObject(); // a new instance each time
    }
}

The same mechanics in plain Java:

live example

import java.util.function.Supplier;

public class ScopeDemo {
    static int created = 0;

    record RequestContext(int id) { }

    static class OrderService {
        private final RequestContext fixed;                 // injected once
        private final Supplier<RequestContext> provider;    // the role of ObjectProvider

        OrderService(RequestContext fixed, Supplier<RequestContext> provider) {
            this.fixed = fixed;
            this.provider = provider;
        }

        void handle() {
            System.out.println("field: #" + fixed.id() + "   provider: #" + provider.get().id());
        }
    }

    public static void main(String[] args) {
        Supplier<RequestContext> factory = () -> new RequestContext(++created);
        OrderService service = new OrderService(factory.get(), factory);

        service.handle();
        service.handle();
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

The field prints #1 both times, the provider prints #2 and #3: ObjectProvider is not an object but a way to ask for one at call time.

Bean lifecycle

From creation to shutdown a bean goes through several phases:

  1. creation (constructor call);
  2. dependency injection;
  3. initialization callbacks — @PostConstruct, then afterPropertiesSet();
  4. wrapping in a proxy (if needed — for example, for @Transactional);
  5. the bean is ready to work;
  6. on application shutdown — @PreDestroy (for singletons only).

The same order shows up without Spring:

live example

public class LifecycleDemo {

    static class PaymentClient {
        String pay(String order) { return "paid " + order; }
    }

    static class OrderService {
        private final PaymentClient payment;   // came in through the constructor
        private String mode;                   // "field injection" — arrives later

        OrderService(PaymentClient payment) {
            this.payment = payment;
            System.out.println("2. constructor: did payment arrive? " + (payment != null));
            System.out.println("   and field mode is still " + mode);
        }

        void afterAssembly() {                 // the role of @PostConstruct
            System.out.println("4. @PostConstruct: mode = " + mode + ", " + payment.pay("A-17"));
        }
    }

    public static void main(String[] args) {
        System.out.println("1. container created the dependency");
        OrderService service = new OrderService(new PaymentClient());

        service.mode = "production";
        System.out.println("3. field injected");
        service.afterAssembly();
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Inside the constructor mode is still null — the field is filled in afterwards. So "after-assembly" logic goes into @PostConstruct, while required dependencies come through the constructor: by the time it runs they are in place.

@Component and @Bean

Two ways to declare a bean, and they are not interchangeable:

  • @Component (and @Service/@Repository/@Controller) go on your own class — Spring will find it during scanning and create it itself.
  • @Bean goes on a method in a @Configuration class — for when you create the object yourself, for example when it's a class from a third-party library that you can't annotate.
@Configuration
class AppConfig {
    @Bean
    ObjectMapper objectMapper() {           // third-party class — configure it by hand
        return new ObjectMapper().findAndRegisterModules();
    }
}

An important detail: @Configuration is wrapped in a proxy, so calling one @Bean method from another returns the same singleton, not a new object. If the @Bean methods live in a plain @Component, there's no such guarantee — dependencies are passed there through method parameters.

Circular dependencies

If A requires B and B requires A through the constructor, Spring can't assemble them and fails on startup (BeanCurrentlyInCreationException): a cycle can't be closed through constructors in principle. A cycle built with field injection used to be resolved by Spring itself, but since Boot 2.6 that is forbidden by default — such an application no longer starts either (the spring.main.allow-circular-references flag brings the old behavior back, but better leave it alone). That's good: a cycle almost always means responsibilities are smeared across classes and they should be split — for example, by extracting shared logic into a third bean. @Lazy and field injection don't cure a cycle, they hide it.

In short

  • IoC is the principle: the container manages creation; DI is the technique: dependencies are passed in from outside.
  • Three injection styles (constructor, setter, field); the constructor wins — final, visible dependencies, testability.
  • ApplicationContext is a BeanFactory plus events, properties, resources, and localization.
  • The default scope is singleton, so beans are made stateless; a prototype inside a singleton is taken through ObjectProvider, otherwise the instance gets fixed once.
  • Bean phases: creation → injection → @PostConstruct → (proxy) → work → @PreDestroy (singletons only).
  • A circular dependency through the constructor breaks startup — a signal to re-split the classes, not a reason to switch on @Lazy.