Spring Boot Fundamentals & Auto-Configuration

Difficulty

The Spring Framework is the underlying platform: the IoC container, the AOP module, Spring MVC, Spring Data, Spring Security, and so on. It's powerful and flexible, but historically required a fair amount of manual configuration — declaring beans explicitly, wiring a DispatcherServlet, configuring a data source, choosing and configuring a servlet container — even for a simple application.

Spring Boot is built on top of Spring, adding an opinionated, convention-over-configuration layer:

  • Auto-configuration: Spring Boot inspects what's on the classpath (and what beans you've already defined) and automatically configures sensible defaults — e.g., if spring-boot-starter-web and an embedded Tomcat are present, it auto-configures a DispatcherServlet and starts an embedded web server, with no explicit XML/Java config required.
  • Starter dependencies: curated, versioned dependency bundles (spring-boot-starter-web, spring-boot-starter-data-jpa) that pull in a coherent, tested set of libraries for a given purpose, instead of manually assembling and version-matching individual JARs.
  • Embedded servers: Tomcat/Jetty/Undertow bundled directly into the runnable JAR — no separate application server installation/deployment needed.
  • Production-ready features: Actuator (health checks, metrics, monitoring endpoints) out of the box.
@SpringBootApplication // Spring Boot's entry point — enables auto-configuration, component scanning, and config
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args); // starts an embedded server, wires everything
    }
}

In one line: Spring Framework is the engine; Spring Boot is the opinionated, batteries-included way of assembling and running that engine with minimal ceremony — every auto-configured default can still be overridden explicitly when the default doesn't fit.

@SpringBootApplication is a single, convenient meta-annotation that bundles together three separate annotations, each contributing a distinct piece of behavior:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication { ... }
  1. @SpringBootConfiguration — itself a specialization of the core Spring @Configuration annotation, marking this class as a source of bean definitions (so @Bean methods inside it are honored) and identifying it, specifically, as the application's primary configuration class (useful for testing tools to locate it).

  2. @EnableAutoConfiguration — the annotation that actually triggers Spring Boot's auto-configuration machinery: based on what's present on the classpath and what beans already exist, Spring Boot conditionally activates a large set of pre-built configuration classes (for a DataSource, a DispatcherServlet, Jackson's ObjectMapper, and so on).

  3. @ComponentScan — scans the package containing the annotated class, and all its subpackages, for @Component/@Service/@Repository/@Controller-annotated classes to register as beans.

That third point is exactly why the conventional Spring Boot project structure places the @SpringBootApplication-annotated main class in the application's root package — anything outside that package tree (and its subpackages) won't be picked up by component scanning unless explicitly configured otherwise.

com.example.myapp
├── MyApp.java              @SpringBootApplication — scans everything under com.example.myapp
├── controller/OrderController.java
├── service/OrderService.java
└── repository/OrderRepository.java

You can use the three annotations individually instead of @SpringBootApplication if you need finer-grained control (e.g., customizing @ComponentScan's base packages independently) — @SpringBootApplication is purely a convenience default for the common case.

Auto-configuration classes are just regular @Configuration classes — nothing magical about their structure — bundled inside Spring Boot's spring-boot-autoconfigure module (and inside third-party starter JARs). What makes them "auto" is two things working together:

1. Discovery: Spring Boot loads a list of candidate auto-configuration classes from a file at META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports on the classpath (this replaced the older spring.factories-based mechanism in Spring Boot 2.7+/3.x). Every listed class is a candidate to be evaluated — not necessarily applied.

2. Conditional activation: each auto-configuration class is guarded by one or more @Conditional-family annotations, so it only actually contributes bean definitions if its conditions hold:

@Configuration
@ConditionalOnClass(DataSource.class)          // only if a DataSource class is on the classpath
@ConditionalOnMissingBean(DataSource.class)    // only if the app hasn't already defined its own DataSource
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
    @Bean
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
}

Common condition annotations:

  • @ConditionalOnClass/@ConditionalOnMissingClass — is a given class present/absent on the classpath (used to detect "is this dependency actually included").
  • @ConditionalOnBean/@ConditionalOnMissingBean — does a bean of a given type already exist in the context (this is exactly what lets your own @Bean definition override an auto-configured default — if you define your own DataSource bean, the auto-configured one backs off).
  • @ConditionalOnProperty — is a specific configuration property set (and optionally, to a specific value).
  • @ConditionalOnWebApplication — is this a web application context at all.

This design is exactly why Spring Boot feels "magical" but remains fully overridable: adding spring-boot-starter-data-jpa to the classpath makes JPA-related conditions become true, activating a cascade of auto-configuration; defining your own equivalent bean explicitly makes the corresponding @ConditionalOnMissingBean condition false, causing Boot's default to quietly step aside in favor of yours.

@EnableAutoConfiguration(exclude = ...) (or the spring.autoconfigure.exclude property) lets you explicitly disable a specific auto-configuration class you don't want, even if its conditions would otherwise be satisfied.

Related Resources

A Spring Boot starter is a special dependency that contains no code of its own — it exists purely as a pom.xml/build.gradle entry whose only purpose is to transitively pull in a curated, mutually-compatible set of other libraries for a specific capability:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Adding just that one line brings in Spring MVC, an embedded Tomcat, Jackson (for JSON), and validation support — all at versions Spring Boot's team has already tested together, rather than requiring you to individually select and version-match each of those libraries yourself.

Why this matters:

  • Eliminates "dependency hell" — historically, assembling a working set of compatible library versions by hand was a common source of runtime NoSuchMethodError/ClassNotFoundException issues from mismatched versions; starters (combined with Spring Boot's dependency management BOM) guarantee a tested, coherent set.
  • Directly drives auto-configuration: adding spring-boot-starter-data-jpa puts Hibernate/Spring Data classes on the classpath, which is exactly the signal @ConditionalOnClass-guarded auto-configuration classes are watching for — the starter and the auto-configuration mechanism are designed to work together.
  • Communicates intent clearlyspring-boot-starter-security in a pom.xml immediately tells a reader "this app uses Spring Security," which a long list of individually-chosen transitive dependencies wouldn't.

Common starters: spring-boot-starter-web (MVC + embedded server), spring-boot-starter-data-jpa (Spring Data JPA + Hibernate), spring-boot-starter-security, spring-boot-starter-test (JUnit, Mockito, AssertJ, Spring Test), spring-boot-starter-actuator (production monitoring endpoints).

Unlike traditional Java EE deployment (build a WAR, deploy it to a separately-installed application server), Spring Boot embeds the servlet container directly inside the application's own runnable JAR — the server runs as a library, inside the same JVM process, started by SpringApplication.run().

Default: spring-boot-starter-web pulls in spring-boot-starter-tomcat transitively, so Tomcat is the out-of-the-box default embedded container.

Switching containers just means adjusting dependencies — exclude the default and add the alternative starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

Auto-configuration detects whichever server's classes are actually present on the classpath and configures that one — no other code changes needed, since Spring Boot's ServletWebServerFactory abstraction (TomcatServletWebServerFactory, JettyServletWebServerFactory, UndertowServletWebServerFactory) hides the container-specific setup behind a common interface.

Configuration happens through the same standard application.properties/application.yml keys regardless of which container is active:

server.port=8443
server.tomcat.max-threads=200
server.compression.enabled=true

Lifecycle: the embedded server starts as part of ApplicationContext refresh and stops as part of context shutdown — there's no separate "deploy" step; running the JAR (java -jar app.jar) both starts the Spring container and the web server together, which is a large part of why Spring Boot applications are simple to containerize (a Docker image just needs a JRE and the JAR).

Choosing between them in practice: Tomcat is the safe, well-understood default for most applications; Undertow is often chosen for a lighter memory footprint and strong non-blocking I/O performance (frequently paired with reactive/WebFlux applications); Jetty is a solid alternative with its own tuning characteristics, sometimes preferred in existing Jetty-based infrastructure.