Skills101

Advanced Java & Spring Boot · Lesson 3 of 16 · Updated ·

Spring Core — IoC Container, Dependency Injection, Beans and Scopes

Before Spring Boot, learn the one idea that Spring is built on: you don't create your objects — the container does.

1. The problem

// Not compiled: the tightly-coupled version we want to avoid
class WelcomeService {
    private EmailNotifier notifier = new EmailNotifier();   // hard-wired
}

WelcomeService is now glued to EmailNotifier. To send SMS instead, or to test without sending real emails, you must edit the class. That is tight coupling.

2. Inversion of Control (IoC) and Dependency Injection (DI)

Instead, the class declares what it needs (usually as constructor parameters), and the Spring IoC container creates the objects and passes them in. Control of object creation is "inverted" from your code to the framework.

The Spring IoC container creates EmailNotifier and SmsNotifier beans and injects them into WelcomeService through its constructor

Objects managed by the container are called beans.

3. A complete example

An interface and two implementations:

// File: course-api/src/main/java/com/skills101/demos/di/Notifier.java
package com.skills101.demos.di;

public interface Notifier {
    String send(String to, String message);
}
// File: course-api/src/main/java/com/skills101/demos/di/EmailNotifier.java
package com.skills101.demos.di;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary   // the default choice when someone asks for "a Notifier"
public class EmailNotifier implements Notifier {

    @Override
    public String send(String to, String message) {
        return "EMAIL to " + to + ": " + message;
    }
}
// File: course-api/src/main/java/com/skills101/demos/di/SmsNotifier.java
package com.skills101.demos.di;

import org.springframework.stereotype.Component;

@Component("sms")   // bean name "sms"
public class SmsNotifier implements Notifier {

    @Override
    public String send(String to, String message) {
        return "SMS to " + to + ": " + message;
    }
}

The class that needs them — note there is no new anywhere:

// File: course-api/src/main/java/com/skills101/demos/di/WelcomeService.java
package com.skills101.demos.di;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class WelcomeService {

    private final Notifier main;
    private final Notifier backup;

    // Spring calls this constructor and passes the beans in — we never write "new EmailNotifier()"
    public WelcomeService(Notifier main, @Qualifier("sms") Notifier backup) {
        this.main = main;
        this.backup = backup;
    }

    public void welcome(String student) {
        System.out.println(main.send(student, "Welcome to Skills101!"));
        System.out.println(backup.send(student, "Your login OTP is 4821"));
    }
}

Start a container and use it:

// File: course-api/src/main/java/com/skills101/demos/di/DiDemo.java
package com.skills101.demos.di;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

public class DiDemo {

    @Configuration
    @ComponentScan   // find @Component / @Service classes in this package
    static class AppConfig {
    }

    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {   // the IoC container
            WelcomeService service = context.getBean(WelcomeService.class);
            service.welcome("Asha");
            // beans are singletons by default: the container hands out the same object every time
            System.out.println("Same object? " + (service == context.getBean(WelcomeService.class)));
            System.out.println("Notifiers: " + context.getBeanNamesForType(Notifier.class).length);
        }
    }
}
<!-- expect: course-api/di.txt -->
EMAIL to Asha: Welcome to Skills101!
SMS to Asha: Your login OTP is 4821
Same object? true
Notifiers: 2
  • @Component marks a class as a bean. @Service, @Repository and @Controller are @Component with a clearer meaning.
  • There are two Notifier beans, so Spring needs a rule: @Primary picks the default, @Qualifier("sms") asks for a specific one by name.
  • Same object? true — beans are singletons by default: one shared instance per container.

4. Three ways to inject

StyleExampleUse?
Constructorpublic WelcomeService(Notifier n)Yes — fields can be final, easy to test, fails fast if missing
Setter@Autowired void setNotifier(Notifier n)for optional dependencies
Field@Autowired private Notifier n;avoid — hidden, hard to test

With a single constructor, @Autowired isn't even needed (as in our example).

5. @Bean methods

For classes you can't annotate (from a library), create beans in a @Configuration class:

// Not compiled: a short sketch — the real version is SecurityConfig in lesson 9
@Configuration
class AppConfig {
    @Bean
    PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
}

We use exactly this pattern for security in a later lesson.

6. Scopes and lifecycle

ScopeInstances
singleton (default)one per container
prototypea new one every time it's requested
request / sessionone per HTTP request / user session (web apps)

Lifecycle: create → inject dependencies → @PostConstruct method → in use → @PreDestroy method when the container closes. Because singletons are shared across threads, don't keep per-user data in fields of a singleton bean.

Common mistakes

  • NoUniqueBeanDefinitionException: two beans of the same type and no @Primary/@Qualifier.
  • NoSuchBeanDefinitionException: the class isn't annotated, or is outside the scanned package.
  • Calling new MyService() yourself — that object isn't a bean, so nothing gets injected into it.

Quick recap

  • IoC: the container creates objects; DI: it passes their dependencies in.
  • @Component / @Service / @Repository / @Controller declare beans; @Bean methods too.
  • Prefer constructor injection; use @Primary / @Qualifier when several beans fit.
  • Default scope is singleton.

Practice what you learned

📝 Quiz: Spring Core and DI5 questions · instant answers & explanations · +10 XP · counts towards your free certificateTake the quiz →