Spring Boot and the IoC Container

How a Spring Boot application starts, and how objects find each other without new
Big idea: In a Spring application you rarely create your service objects yourself. You declare classes as components, and a container called the ApplicationContext builds them, wires their dependencies together, and hands you finished objects. Spring Boot adds auto-configuration and starters so that a working web + database + security stack comes up from almost no configuration.

What You Will Learn

  • What "Inversion of Control" and "Dependency Injection" mean.
  • What @SpringBootApplication actually switches on.
  • How auto-configuration and starter dependencies work together.
  • The stereotype annotations: @Component, @Service, @Repository, @RestController, @Configuration.
  • Constructor injection, and why the project uses Lombok's @RequiredArgsConstructor.
  • Defining beans explicitly with @Bean methods.
  • Reading configuration with @Value and application.yaml.

Chapter Structure

1.1 Inversion of Control1.2 The application entry point1.3 Auto-configuration & starters1.4 Beans & stereotypes1.5 Dependency injection1.6 @Bean methods1.7 External configuration1.8 Wrap-up

1.1 Inversion of Control (IoC)

Normally your code decides when to create objects:

// Without a container — you build the whole graph by hand
var userRepository   = new UserRepository(dataSource);
var passwordEncoder  = new BCryptPasswordEncoder();
var jwtUtil          = new JwtUtil(secret, expiration);
var authService      = new AuthService(userRepository, passwordEncoder, jwtUtil, revokedTokenRepository);

Every class needs to know how to construct everything it depends on. With Inversion of Control that responsibility is flipped: a container creates the objects and supplies each one with what it needs. Your class just says "I need an AuthService" and receives one.

Dependency Injection (DI) is the specific technique Spring uses to do this — it "injects" collaborators through the constructor.

Remember: IoC is the principle ("something else owns object creation"). DI is the mechanism ("dependencies arrive through the constructor"). The container that does it in Spring is the ApplicationContext.

1.2 The Application Entry Point

The hospital-erp backend starts from a single tiny class:

package com.example.hospital_erp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HospitalErpApplication {

    public static void main(String[] args) {
        SpringApplication.run(HospitalErpApplication.class, args);
    }
}

SpringApplication.run(...) does a lot:

  1. Creates the ApplicationContext.
  2. Component scanning — looks through the package of this class and all sub-packages for annotated classes (controllers, services, repositories, configs).
  3. Runs auto-configuration.
  4. Instantiates every bean and injects dependencies in the right order.
  5. Starts the embedded web server (Tomcat) on the configured port.
Why package location matters: component scanning starts at com.example.hospital_erp because that is where HospitalErpApplication lives. Every feature package — auth, patient, appointment, reports — sits under it, so all their beans are found automatically. A class placed in a sibling package outside that root would be silently ignored.

1.2.1 What @SpringBootApplication expands to

It is a convenience annotation that combines three:

AnnotationEffect
@SpringBootConfigurationMarks this class as a source of bean definitions (a specialised @Configuration).
@EnableAutoConfigurationTurns on Spring Boot's "configure things based on what is on the classpath" behaviour.
@ComponentScanScans this package and below for @Component and friends.

1.3 Auto-Configuration and Starters

A starter is a curated bundle of dependencies. The project's pom.xml pulls in:

spring-boot-starter-webmvc        → REST controllers + embedded Tomcat + Jackson JSON
spring-boot-starter-data-jpa     → Hibernate + Spring Data + connection pool
spring-boot-starter-security     → Spring Security filter chain
spring-boot-starter-validation   → Bean Validation (Jakarta)
mysql-connector-j                → the MySQL JDBC driver
lombok                           → boilerplate reduction (compile-time)

Auto-configuration reacts to those jars. Because spring-boot-starter-data-jpa and a MySQL driver are on the classpath and a spring.datasource.url is set, Spring Boot automatically builds a DataSource, an EntityManagerFactory, and a JpaTransactionManager — none of which appear anywhere in the project's code.

jar on classpathmatching @Conditional auto-configbeans createdyou override only what you need

The project overrides exactly one piece of security auto-configuration — the SecurityFilterChain — in config/SecurityConfig.java. Everything else is left on its defaults. That is the intended style: lean on the defaults, replace only the specific bean you care about.

1.4 Beans and Stereotype Annotations

A bean is simply an object the container manages. The most common way to declare one is a stereotype annotation on a class. They are all @Component underneath; the different names document intent and sometimes add behaviour.

AnnotationLayerExample in hospital-erp
@RestControllerWeb / APIPatientController, AuthController
@ServiceBusiness logicPatientService, VisitService, AuthService
@RepositoryPersistencePatientRepository (actually an interface — see Chapter 3)
@ComponentAnything elseJwtUtil, JwtAuthFilter
@ConfigurationBean definitionsSecurityConfig
Memory trick: @Repository talks to the database, @Service holds the rules, @RestController talks to the outside world, @Component is the generic fallback.

1.4.1 Singleton scope

By default every bean is a singleton: the container makes one instance and shares it everywhere. There is one PatientService object for the whole application, used by every HTTP request at the same time.

Consequence: because beans are shared across concurrent requests, do not store per-request data in instance fields of a service. Keep services stateless — pass data in as method arguments, as every service in this project does.

1.5 Dependency Injection in Practice

Spring supports field, setter and constructor injection. Constructor injection is the recommended form and the only one used in this codebase. Here is AuthService:

@Service
@RequiredArgsConstructor          // Lombok generates the constructor
public class AuthService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;
    private final RevokedTokenRepository revokedTokenRepository;

    // ... methods use those four fields ...
}

There is no @Autowired and no hand-written constructor. Two things combine:

  • Lombok's @RequiredArgsConstructor generates a constructor taking every final field.
  • Spring sees a single constructor and injects a matching bean for each parameter automatically (no annotation needed since Spring 4.3).
Why constructor injection is preferred:
  • Dependencies can be final → the object is fully built and immutable once constructed.
  • Impossible to create the object in an invalid, half-injected state.
  • Dependencies are visible in one place; a constructor with 10 parameters is a smell telling you the class does too much.
  • Trivial to unit test — just call new with mocks.

1.5.1 How Spring resolves a parameter

constructor needs UserRepositorycontext has exactly one bean of that typeinject it

If no bean matches, startup fails with NoSuchBeanDefinitionException. If more than one matches, startup fails with NoUniqueBeanDefinitionException unless you disambiguate with @Qualifier or @Primary. Failing at startup rather than at request time is deliberate.

1.6 Defining Beans with @Bean Methods

Stereotypes work when you own the class. When the object comes from a library you cannot annotate, declare it inside a @Configuration class. From SecurityConfig:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                // ...
                .build();
    }
}

Key points:

  • The method return value becomes a bean; its type is what gets injected elsewhere. That is why AuthService can depend on the interface PasswordEncoder even though the concrete class is BCryptPasswordEncoder.
  • A @Bean method's parameters are themselves injected — HttpSecurity and AuthenticationConfiguration are beans Spring Boot auto-configured.
  • The bean name defaults to the method name (passwordEncoder).

1.7 External Configuration

Values that change between environments do not belong in code. The project keeps them in src/main/resources/application.yaml:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/aarogya_hms?useSSL=false&serverTimezone=UTC
    username: root
    password: qwerty@12345
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: true

server:
  port: 8080

jwt:
  secret: aG9zcGl0YWwtZXJwLXNlY3JldC1rZXktbXVzdC1iZS...
  expiration: 86400000

spring.* keys are consumed by auto-configuration. Custom keys like jwt.* are read wherever you want with @Value. From JwtUtil:

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;
    // ...
}
Precedence: the same key can be supplied by (in increasing priority) application.yaml, an environment-specific file such as application-prod.yaml (activated with SPRING_PROFILES_ACTIVE=prod), OS environment variables (JWT_SECRET), and JVM system properties. This is how the hard-coded secret and DB password in the file should be overridden in a real deployment — see Chapter 7.

1.7.1 @Value vs @ConfigurationProperties

@Value("${jwt.secret}")@ConfigurationProperties("jwt")
One key at a time, injected into a field.Binds a whole group of keys onto a typed object.
Fine for one or two values (what this project does).Better once a prefix has many keys; supports validation and IDE metadata.

Quick Concept Map

IoC → the container owns object creation

DI → dependencies arrive via the constructor

@SpringBootApplication → config + auto-config + component scan

Starter → a bundle of dependencies → auto-configuration reacts to it

Bean → an object Spring manages (singleton by default)

@Service / @RestController / @Repository / @Component → class-level bean declarations

@Bean method → bean declaration for types you do not own

@Value + application.yaml → configuration outside code

Common Mistakes

  1. Putting a component in a package that is not under the main application class — it never gets scanned.
  2. Calling new PatientService(...) yourself instead of injecting it, so its own dependencies are null.
  3. Using field injection (@Autowired on a field) — the field cannot be final and the class is hard to test.
  4. Storing request-specific state in a singleton bean's fields.
  5. Two beans of the same type with no @Primary / @Qualifier, causing a startup failure.
  6. Forgetting that @Value needs the property to exist — a missing jwt.secret fails startup unless a default is given: @Value("${jwt.secret:changeme}").
  7. Committing real secrets to application.yaml instead of supplying them per-environment.

Revision Questions

  1. In one sentence each, define IoC and DI.
  2. Name the three annotations @SpringBootApplication combines.
  3. What determines which packages are component-scanned?
  4. What is a "starter" and how does auto-configuration use it?
  5. Why is constructor injection preferred over field injection?
  6. How does @RequiredArgsConstructor cooperate with Spring's injection?
  7. When do you need a @Bean method instead of @Service?
  8. What scope are beans by default, and what does that imply about state?
  9. Where would you put the production database password instead of application.yaml?

Practice

1. Trace the graph. Open AuthService and list every bean Spring must create before it can construct AuthService.
2. Add a component. Create a @Component class UhidGenerator and inject it into PatientService in place of the private helper methods.
3. Externalise a value. Move the "AAR-%06d" patient-code prefix from PatientService into application.yaml and read it with @Value.

Chapter Wrap-Up

A Spring Boot application is a graph of beans that the ApplicationContext builds at startup. @SpringBootApplication switches on component scanning and auto-configuration; starters put libraries on the classpath and auto-configuration turns them into working infrastructure. You declare your own classes with stereotype annotations, receive their collaborators through the constructor, and reach for @Bean methods only for types you do not own. Configuration that varies by environment lives in application.yaml and is read with @Value.

Next chapter: we follow an HTTP request into a @RestController and see how Spring MVC turns a URL and a JSON body into a Java method call.

Teaching note: examples are adapted from the hospital-erp backend (Spring Boot 4, Java 25) for learning clarity. Package names and class names match that project so you can open each file as you read.