Inversion of Control (IoC)

In traditional Java, objects create their own dependencies (new MyService()). IoC inverts this: a container creates and wires objects. You describe components; Spring assembles the graph.

Benefits:

  • Loose coupling — depend on interfaces, not implementations
  • Testability — inject mocks in tests
  • Centralized configuration — change wiring without code changes

Spring Container

The ApplicationContext is Spring’s IoC container:

  ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
UserService service = ctx.getBean(UserService.class);
  

In Spring Boot, the container starts automatically — you rarely interact with it directly.

Defining Beans

Component Scanning

  @Configuration
@ComponentScan("com.example.app")
public class AppConfig {}
  

Classes annotated with stereotypes are auto-registered:

  @Service
public class UserService {
    // business logic
}

@Repository
public class JpaUserRepository implements UserRepository {
    // data access
}

@Controller
public class UserController {
    // web layer
}
  

Java Configuration

  @Configuration
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
        ds.setUsername("user");
        ds.setPassword("pass");
        return ds;
    }

    @Bean
    public UserRepository userRepository(DataSource dataSource) {
        return new JdbcUserRepository(dataSource);
    }
}
  

@Bean methods define factory-created objects. Method parameters are auto-injected.

Dependency Injection

  @Service
public class OrderService {
    private final OrderRepository repository;
    private final PaymentGateway gateway;

    public OrderService(OrderRepository repository, PaymentGateway gateway) {
        this.repository = repository;
        this.gateway = gateway;
    }

    public Order placeOrder(OrderRequest req) {
        Order order = repository.save(new Order(req));
        gateway.charge(order.getTotal());
        return order;
    }
}
  

Constructor injection makes dependencies explicit and enables final fields. Spring 4.3+ auto-wires single-constructor beans without @Autowired.

Interface-Based Design

  public interface NotificationService {
    void send(String to, String message);
}

@Service
public class EmailNotificationService implements NotificationService {
    @Override
    public void send(String to, String message) {
        // send email
    }
}
  

Inject the interface — swap implementations via configuration or profiles.

Bean Scopes

Scope Description
singleton One instance per container (default)
prototype New instance per request
request One per HTTP request (web apps)
session One per HTTP session (web apps)
  @Component
@Scope("prototype")
public class ReportGenerator { }
  

Default singleton is correct for stateless services. Use prototype for stateful or per-operation objects.

Bean Lifecycle

  @Component
public class CacheWarmer {

    @PostConstruct
    public void init() {
        // runs after dependency injection
        System.out.println("Warming cache...");
    }

    @PreDestroy
    public void cleanup() {
        // runs before container shutdown
        System.out.println("Releasing resources...");
    }
}
  

Implement InitializingBean / DisposableBean only if you need framework-level hooks — annotations are preferred.

Qualifiers and Primary

When multiple beans implement the same interface:

  @Service
@Primary
public class StripePaymentGateway implements PaymentGateway { }

@Service
@Qualifier("paypal")
public class PayPalPaymentGateway implements PaymentGateway { }

@Service
public class CheckoutService {
    private final PaymentGateway defaultGateway;
    private final PaymentGateway paypalGateway;

    public CheckoutService(
            PaymentGateway defaultGateway,
            @Qualifier("paypal") PaymentGateway paypalGateway) {
        this.defaultGateway = defaultGateway;
        this.paypalGateway = paypalGateway;
    }
}
  

@Primary marks the default; @Qualifier selects by name.

Profiles

  @Service
@Profile("dev")
public class MockEmailService implements EmailService {
    @Override
    public void send(String to, String body) {
        System.out.println("DEV: Would send to " + to);
    }
}

@Service
@Profile("prod")
public class SmtpEmailService implements EmailService {
    @Override
    public void send(String to, String body) {
        // real SMTP
    }
}
  

Activate: spring.profiles.active=dev

Aspect-Oriented Programming (AOP)

Separate cross-cutting concerns from business logic:

  @Aspect
@Component
public class LoggingAspect {

    @Around("@annotation(Timed)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long elapsed = System.currentTimeMillis() - start;
        System.out.println(joinPoint.getSignature() + " took " + elapsed + "ms");
        return result;
    }
}
  
  @Service
public class ReportService {
    @Timed
    public Report generate() {
        // business logic only — no logging boilerplate
    }
}
  

Common AOP uses: logging, transaction management (@Transactional), security checks, caching (@Cacheable).

Conditional Beans

  @Bean
@ConditionalOnProperty(name = "feature.new-checkout", havingValue = "true")
public CheckoutServiceV2 checkoutServiceV2() {
    return new CheckoutServiceV2();
}
  

Spring Boot uses conditions extensively for auto-configuration.

Testing the Container

  @ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
class UserServiceTest {

    @Autowired
    private UserService userService;

    @MockBean
    private UserRepository userRepository;

    @Test
    void findsUser() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(new User("Alice")));
        assertEquals("Alice", userService.getName(1L));
    }
}
  

@MockBean replaces a container bean with a Mockito mock.

Best Practices

  • Program to interfaces; inject implementations
  • Constructor injection with final fields
  • Keep @Configuration classes focused — one per concern
  • Avoid circular dependencies — redesign if they appear
  • Use profiles for environment-specific beans

Spring Core is the foundation — every other Spring project builds on the IoC container and bean lifecycle.