What is Spring Boot? AutoConfig, Starters & Embedded Servers
Understand Spring Boot fundamentals: how auto-configuration works, using starter POMs, deploying with embedded servers, and production-ready features.
Understand Spring Boot fundamentals: how auto-configuration works, using starter POMs, deploying with embedded servers, and production-ready features. The guide uses practical examples to explain introduction to spring boot, auto-configuration deep dive and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.
What is Spring Boot? Auto-Configuration, Starter POMs, Embedded Servers
Before Spring Boot, spinning up a Spring web app meant wrestling with XML configuration files, hunting down compatible library versions by trial and error, and writing boilerplate just to expose a single HTTP endpoint. Here is what that looked like:
<!-- web.xml - half a page of XML just to set up a Servlet -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.example.AppConfig</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- Application config - another XML file -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="org.postgresql.Driver"/>
<property name="url" value="jdbc:postgresql://localhost:5432/mydb"/>
<!-- and on and on... -->
</bean>
Spring Boot replaces all of that ceremony with three ideas that work together:
- Auto-configuration figures out what your application needs and sets it up automatically
- Starter POMs pull in the right dependencies at compatible versions in a single line
- Embedded servers let you ship a runnable JAR instead of deploying to an application server
The same application in Spring Boot:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class GreetingController {
@GetMapping("/hello/{name}")
String hello(@PathVariable String name) {
return "Hello, " + name + "!";
}
}
No XML. No version hunting. Run java -jar app.jar and you have a running web server on port 8080.
Introduction to Spring Boot
Spring Boot sits on top of the Spring Framework and applies a convention-over-configuration philosophy. You get a running application without installing an external app server, without tracking down which library versions work together, and without XML wiring. Starter POMs bring in dependencies at locked versions. Health checks, metrics, and externalized configuration ship in without extra libraries.
When you add spring-boot-starter-web, Spring Boot detects Spring MVC on the classpath and spins up an embedded Tomcat, wires a DispatcherServlet, and sets up view resolution — all automatically, all without any XML or Java configuration you would have written before.
graph TB
subgraph "Spring Boot Layer"
A[Starter POMs] --> B[Auto-Configuration]
B --> C[Embedded Servers]
end
subgraph "Spring Framework"
D[Spring Core] --> E[Spring MVC]
E --> F[Spring Data]
end
subgraph "Application"
G[Your Code] --> B
end
B --> D
B --> E
B --> F
Spring Boot works with Maven and Gradle. Your build file declares which starters you want, and the Spring Boot Maven plugin packages everything into an executable JAR with all dependencies and a main class that runs immediately.
Auto-Configuration Deep Dive
Auto-configuration is what makes Spring Boot useful. It pokes around your environment and automatically configures beans you would otherwise have to define by hand. This happens through configuration class conditional loading.
How Auto-Configuration Works
When a Spring Boot application starts, the AutoConfigurationImportFilter examines every class on the classpath. This filter reads from META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Spring Boot 3.x) or the older spring.factories file (Spring Boot 2.x) to find all available auto-configuration classes.
Each auto-configuration class has conditional annotations that determine whether it should activate. Most conditions check whether a class exists on the classpath, whether a bean of a certain type already exists, or whether a property in application.properties has a particular value.
@Configuration(proxyBeanMethods = false) // No CGLIB proxy needed for this config
@AutoConfigureAfter(DataSourceConfiguration.class) // Run after any explicit DataSource config
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class }) // Only activate if these are on the classpath
@ConditionalOnProperty(
prefix = "spring.datasource",
name = "enabled",
havingValue = "true",
matchIfMissing = true // Activate by default if property is absent
)
public class DataSourceAutoConfiguration {
@ConditionalOnMissingBean(type = "javax.sql.DataSource") // Don't override user-defined beans
@Bean
public DataSource dataSource() {
// Create a DataSource based on detected configuration
// (auto-configured username, password, URL from application.properties)
}
}
In this example, DataSourceAutoConfiguration activates only when the classpath has javax.sql.DataSource and JdbcTemplate, when no other DataSource bean exists, and when spring.datasource.enabled is not set to false.
Key Conditional Annotations
Knowing these annotations helps you debug configuration problems and turn off what you do not need:
| Annotation | Activates When |
|---|---|
@ConditionalOnClass |
A specific class exists on the classpath |
@ConditionalOnMissingClass |
A specific class does NOT exist on the classpath |
@ConditionalOnBean |
A bean of a specific type already exists |
@ConditionalOnMissingBean |
No bean of a specific type exists |
@ConditionalOnProperty |
A property matches a specific value |
@ConditionalOnWebApplication |
Running in a web application context |
@ConditionalOnNotWebApplication |
Running in a non-web context |
The matchIfMissing = true attribute on @ConditionalOnProperty makes the configuration activate even when the property is absent. That is how defaults work without forcing you to set every possible option.
Disabling Auto-Configuration
When auto-configuration conflicts with what you need, you can disable specific configurations without losing the rest. The most direct way uses the exclude attribute on @SpringBootApplication:
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
You can also disable configurations in application.properties:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
This fine-grained control means you get auto-configuration for everything except the specific technologies where you need custom setup.
Starter POMs
Starter POMs solve the version compatibility problem between libraries. Each starter bundles a set of transitive dependencies at compatible versions, so you stop chasing compatibility matrices.
How Starter POMs Work
A starter POM has no code. It only declares dependencies. When you put spring-boot-starter-web in your pom.xml, you get Spring MVC, Spring Boot’s web auto-configuration, Tomcat as the default embedded server, and Jackson for JSON serialization, all at versions that have been tested to work together.
Naming follows a pattern:
spring-boot-starter-*— Application starters for common use casesspring-boot-starter-test— Testing with JUnit, Mockito, AssertJspring-boot-autoconfigure-*— Auto-configuration modules (usually pulled in transitively)spring-boot-*— Spring Boot’s own modules (actuator, devtools)
Essential Starters
Most Spring Boot projects end up using the same starters:
Core Starters:
<!-- The one that makes it Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Web applications (includes Tomcat) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Reactive web applications -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Bean validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Data Starters:
<!-- JPA with Hibernate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MongoDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Security and Ops:
<!-- Authentication and authorization -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Production-ready monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Choosing the Right Starter
When you are not sure which starter to add, work backwards from what your application should do:
- Need HTTP endpoints? →
spring-boot-starter-web(orspring-boot-starter-webfluxfor reactive) - Need to store data in a relational database? →
spring-boot-starter-data-jpaplus a driver (spring-boot-starter-data-jpabrings Hibernate, you addpostgresqlormysqldriver) - Need to secure those endpoints? → Stack
spring-boot-starter-securityon top - Need to validate input data? → Add
spring-boot-starter-validation - Need visibility into what is happening at runtime? → Add
spring-boot-starter-actuator
spring-boot-starter alone gives you auto-configuration, property support, and the Spring Boot CLI. Build from there.
Embedded Servers
Before embedded servers, deploying a Java web app meant installing an application server (Tomcat, Jetty, WildFly), configuring it through XML, and dropping a WAR file into it. Spring Boot changed that by bundling the servlet container inside your application JAR.
How Embedded Servers Work
When you include spring-boot-starter-web, Spring Boot’s auto-configuration detects an embedded servlet container and creates the beans needed to start it. The EmbeddedServletContainerAutoConfiguration class activates based on which server is on the classpath, and each server (Tomcat, Jetty, Undertow) has its own auto-configuration class.
Your code still uses the standard Servlet API without changes. What changes is that the server lifecycle (start, stop, port binding) is managed by Spring Boot instead of an external container.
graph LR
A[Application JAR] --> B[Main Method]
B --> C[Spring Boot Auto-configuration]
C --> D[Embedded Server]
D --> E[Servlet Container]
E --> F[DispatcherServlet]
F --> G[Your Controllers]
Tomcat, Jetty, and Undertow Comparison
Each embedded server has its own character. Tomcat is the default because it needs no extra native dependencies and works everywhere, but the other two make sense in specific situations.
| Characteristic | Tomcat | Jetty | Undertow |
|---|---|---|---|
| Default | Yes | No | No |
| Memory Footprint | Medium (~60MB heap) | Light (~25MB heap) | Lightest (~15MB heap) |
| HTTP/2 Support | Yes (with APR) | Yes | Yes (native) |
| WebSocket Support | Yes | Yes | Yes |
| Servlet Compatibility | 4.0 | 3.1 | 3.1 |
| NIO by Default | Yes | Yes | Yes |
| Common Use Case | General purpose | Lightweight services | High-performance microservices |
To swap Tomcat for Jetty, exclude Tomcat and add Jetty in your pom.xml:
<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-jetty</artifactId>
</dependency>
For Undertow, use spring-boot-starter-undertow instead.
Server Configuration
All three servers share common configuration properties. Put these in application.properties:
# Port (default: 8080)
server.port=8080
# Context path (default: /)
server.servlet.context-path=/api
# Compression
server.compression.enabled=true
server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
# Connection timeout (milliseconds)
server.connection-timeout=20000
# Max threads
server.tomcat.threads.max=200
server.tomcat.threads.min-spare=10
Tomcat-specific tuning:
server.tomcat.max-connections=10000
server.tomcat.accept-count=100
server.tomcat.max-keep-alive-requests=100
When to Use Spring Boot
Spring Boot works well when you need to ship working software fast without giving up production-readiness. It pays off most for services that will stick around and need maintenance over years, not throwaway prototypes.
Strong Fit:
- RESTful API services, especially with multiple endpoints needing consistent error handling
- Microservices where each service deploys and scales independently
- Applications persisting data to relational databases with JPA or Spring Data
- Services that need authentication and authorization
- Any project where you want production-ready observability without extra setup
Weaker Fit:
- Batch jobs on a schedule with no HTTP interface — look at Spring Batch instead
- Applications with extreme memory constraints where even 15MB is too much — consider something leaner or the raw Servlet API
- Teams already invested in a different ecosystem where Java is not already in the stack
Common Pitfalls
Spring Boot reduces complexity but brings its own traps for developers who have not seen it before.
Over-relying on auto-configuration. It is a useful default, but it hides what is actually happening. When something breaks, developers who do not understand the underlying Spring concepts struggle to figure out why.
Ignoring the classpath. Starters pull in so many transitive dependencies that it is easy to end up with conflicting libraries. If you see strange NoSuchMethodError or ClassNotFoundException at runtime, run mvn dependency:tree to see exactly which versions landed on the classpath.
Forgetting to externalize configuration. Hardcoded values work in development and break in production. Use application.properties, environment variables, or a configuration server. Spring Boot’s relaxed binding maps environment variables like DATABASE_HOST to properties like spring.datasource.host.
Using default datasource settings in production. The embedded H2 database is convenient for development but has no business in production. Always configure an external database and tune your connection pool settings (HikariCP is the default) for your expected load.
Not disabling security in tests. Once you add spring-boot-starter-security, every endpoint requires authentication by default. Use @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) with @AutoConfigureMockMvc or explicitly configure security for tests.
Security Notes
Security in Spring Boot comes through spring-boot-starter-security, which sets up a baseline that protects all endpoints except /actuator/health. The default uses HTTP Basic authentication with a generated password printed to the console on startup.
Hardening steps for any Spring Boot application:
Change the default user and password in application.properties:
spring.security.user.name=admin
spring.security.user.password=<use-a-strong-password-manager>
Or skip the default user entirely and use a real user details service:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
}
For production, use OAuth2 or JWT instead of session-based auth. The spring-boot-starter-oauth2-resource-server starter makes the configuration straightforward:
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://your-issuer.com
Dependency scanning should run in your CI pipeline. The OWASP Dependency Check Maven plugin looks for known vulnerabilities in your dependencies:
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.0</version>
</plugin>
Spring Boot’s release train means the team maintains security patches for older versions, but that does not cover vulnerabilities in third-party libraries.
Implementation Snippets
Minimal Project Structure
A Spring Boot project needs very little to get going. After the Maven pom.xml, you only need the application class and an optional application.properties.
src/
main/
java/
com/
example/
demo/
DemoApplication.java # Main class with @SpringBootApplication
resources/
application.properties # Configuration
test/
java/
com/
example/
demo/
DemoApplicationTests.java # Basic test
pom.xml
Minimal pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Main Application Class
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication is a composite annotation that combines @Configuration, @EnableAutoConfiguration, and @ComponentScan. You almost never need to write these separately.
A Simple REST Controller
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
@GetMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return "Hello, " + name + "!";
}
}
Component scanning registers this controller automatically. No web.xml, no extra configuration needed. Run the app and curl localhost:8080/hello/World gives you Hello, World!.
Observability Checklist
Spring Boot Actuator gives you production-ready monitoring out of the box. Add the starter and you get health endpoints, metrics, and environment information out of the box.
Add the actuator starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Configure what Actuator exposes:
# Health endpoint details (default: never)
management.endpoint.health.show-details=when_authorized
# Enable specific endpoints
management.endpoints.web.exposure.include=health,info,metrics,env,beans
# Health check customizations
management.health.livenessState.enabled=true
management.health.readinessState.enabled=true
# Metrics export (Prometheus format)
management.prometheus.metrics.export.enabled=true
What to monitor in production:
GET /actuator/health— Overall application health, pair with Kubernetes liveness/readiness probesGET /actuator/metrics/http.server.requests— HTTP request timing, error ratesGET /actuator/metrics/jvm.memory.used— Memory pressure early warningGET /actuator/info— Build information including git commit SHA for traceabilityGET /actuator/beans— All Spring beans (useful in development, restrict in production)
Correlation IDs link logs across service calls. Add a filter that extracts or generates a correlation ID and puts it in the MDC:
@Component
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String correlationId = request.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
MDC.put("correlationId", correlationId);
response.setHeader(CORRELATION_ID_HEADER, correlationId);
filterChain.doFilter(request, response);
}
}
Trade-off Table
Auto-Configuration Pros and Cons
| Aspect | Pro | Con |
|---|---|---|
| Speed | Fast project startup with minimal configuration | Hidden complexity makes debugging harder |
| Defaults | Sensible defaults work out of the box | Defaults may not fit production needs |
| Version Management | Starter POMs handle transitive dependencies | Less visibility into what versions are used |
| Customization | Override anything with explicit configuration | Knowing what to override takes experience |
| Learning Curve | Hides Spring complexity for beginners | Hides Spring complexity for beginners |
Embedded Server Comparison
| Server | Best For | Avoid When |
|---|---|---|
| Tomcat | General web apps, beginners, maximum compatibility | You need minimum memory footprint |
| Jetty | Long-running connections, lower memory than Tomcat | You need the absolute fastest response times |
| Undertow | High-performance microservices, non-blocking I/O | You need broad servlet spec compliance or beginner-friendly docs |
Failure Scenarios
Application starts but returns 404 on all endpoints. Usually a missing @RestController or @RequestMapping. Check that your controller has the annotations and lives in a package Spring Boot’s component scan covers (by default, the main class’s package and everything under it).
Bean creation fails with “required a bean of type X not found.” Auto-configuration did not activate for whatever provides X. Verify the right starter is on the classpath, its conditions are met, and you have not accidentally excluded its auto-configuration class.
OutOfMemoryError: PermGen space or Metaspace. Shows up with Spring Boot 1.x on HotSpot JVM. Spring Boot 2+ uses Metaspace by default. Fix by setting -XX:MaxMetaspaceSize=256m (tune as needed) and make sure you are not leaking classloaders through dynamic classloading.
Embedded server fails to start on port 8080. Port conflicts happen often in shared dev environments. Set server.port=0 to let Spring Boot pick an available port, or use the SERVER_PORT environment variable.
App works locally but fails in production with DataSource errors. Production typically uses a different database than H2. Verify spring.datasource.url is set correctly for production, credentials are externalized and not in source control, and the database driver JAR is in the packaged app.
ClassNotFoundException at runtime with AOT compilation. Spring Boot 3.x with GraalVM native image compilation has limits. Not all Spring features work in AOT mode. Test your native image build in CI before betting on it.
Interview Questions
@SpringBootApplication is a composite annotation that bundles three things: @Configuration (marks the class as a source of bean definitions), @EnableAutoConfiguration (turns on Spring Boot's auto-configuration), and @ComponentScan (enables component scanning in the package hierarchy).
@ComponentScan without arguments scans the package of the annotated class and all sub-packages for components like @Controller, @Service, and @Repository. If you want to scan packages outside your main app's package tree, specify them explicitly with @ComponentScan(basePackages = "com.example").
Auto-configuration relies on conditional annotations on each configuration class. The framework evaluates conditions like @ConditionalOnClass (does a required class exist on the classpath?), @ConditionalOnMissingBean (has the user already defined this bean?), and @ConditionalOnProperty (does a configuration property match a certain value?). Only when all conditions on a configuration class are satisfied does it become active.
The spring.factories file (Spring Boot 2.x) or AutoConfiguration.imports file (Spring Boot 3.x) lists all available auto-configuration classes. The actual activation decision happens at runtime during Spring context refresh.
Exclude the spring-boot-starter-tomcat transitive dependency from spring-boot-starter-web and add spring-boot-starter-jetty as an explicit dependency. In Maven, use the <exclusions> element inside the web starter dependency to remove Tomcat, then add a separate dependency on the Jetty starter. Spring Boot's auto-configuration detects Jetty on the classpath and sets it up automatically.
Starter POMs are dependency descriptors following the spring-boot-starter-* naming convention. They have no code, only dependencies. When you add a starter, you pull in a curated set of transitive dependencies at versions the Spring Boot team has verified for compatibility. This comes through the spring-boot-starter-parent POM, which defines version properties and dependency management sections that all starters inherit.
The practical benefit is that you stop spending time on "which version of Apache Commons Codec works with Spring Framework 6.1?" The answer is whatever version the Spring Boot team has already validated.
With an embedded server, your application runs as a self-contained process with java -jar app.jar. The server lifecycle lives inside your application — simpler deployment, but no shared server resources or server-level management tools. With a WAR deployed to an external server, multiple applications share the same server instance and you get centralized management, but you deal with version alignment between your app and the server.
Embedded servers dominate microservices and cloud-native deployments because they fit container packaging naturally. Traditional enterprise environments with centralized app server management still benefit from WAR deployment.
Auto-configuration does not fail silently — when a conditional is not met, the configuration class simply does not activate and no bean is created. The confusion usually comes from assuming a feature is set up when its conditions were never satisfied.
Debug it by enabling auto-configuration logging in application.properties:
logging.level.org.springframework.boot.autoconfigure=DEBUG
This prints every auto-configuration decision: which classes were evaluated, which conditions passed, and which were skipped and why. Also check /actuator/beans at runtime to see exactly which beans were registered.
These two annotations are opposites in intent:
@ConditionalOnBean(type = "DataSource.class")
// Activates ONLY if a DataSource bean already exists
@ConditionalOnMissingBean(type = "DataSource.class")
// Activates ONLY if NO DataSource bean exists
In auto-configuration, @ConditionalOnMissingBean is the critical guard that prevents your auto-config from overriding beans a developer explicitly defined. Always pair auto-configuration with @ConditionalOnMissingBean on any bean-producing method — otherwise you silently replace user-defined beans, which causes hard-to-diagnose bugs.
@ConditionalOnBean is less common in auto-config but useful when your configuration depends on something the user is expected to provide first.
The spring-boot-starter-parent defines version properties for hundreds of third-party libraries and locks them in its <dependencyManagement> section. When you write <dependency>spring-boot-starter-web</dependency> without a version, the parent POM resolves it from its managed versions — so you get Spring Framework, Tomcat, Jackson, and Hibernate at compatible versions without touching a compatibility matrix.
Override any version by specifying it explicitly in your pom.xml, but that puts version management back on you.
spring-boot-starter-web uses Spring MVC with a blocking, synchronous model. Every request occupies a thread while waiting for I/O (database, external API calls). Under load, you need more threads to handle more concurrent requests.
spring-boot-starter-webflux uses Spring WebFlux with Netty under the hood and a reactive, non-blocking model. Requests do not hold threads while waiting — they subscribe to a stream of data and resume when data arrives. This handles very high concurrency with far fewer threads.
// Spring MVC — blocking
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id); // thread blocks here
}
// WebFlux — non-blocking
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userRepository.findById(id); // returns immediately, data comes async
}
Use WebFlux when you have many concurrent slow operations (multiple external API calls, streaming data). Use MVC when your code is CPU-bound or you need maximum ecosystem compatibility — the reactive ecosystem is still narrower than the blocking one.
Both configure Spring Boot — they are just two syntaxes for the same configuration. application.properties uses a flat key-value format:
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
application.yml uses hierarchical YAML syntax:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: admin
YAML is more readable for deeply nested properties and is the preferred format in Spring Boot. Properties files are still common when you need to externalize config via environment variables directly (since ${SPRING_DATASOURCE_URL} syntax works cleanly in properties).
One practical difference: YAML files are loaded in a specific order and merged. Properties files take precedence in some overlapping scenarios. When in doubt, prefer one format and stick with it.
Auto-configuration handles it by default when the right dependencies are on the classpath. The simplest approach is setting properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.datasource.password=secret
spring.datasource.driver-class-name=org.postgresql.Driver
Spring Boot detects the driver from the URL and auto-configures HikariCP as the connection pool with sensible defaults. Tune the pool via HikariCP-specific properties:
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
For full control, define your own DataSource bean — this disables auto-configuration for that bean specifically (thanks to @ConditionalOnMissingBean on the auto-config):
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(myCustomConfig());
}
}
Spring Boot's default error handling returns a structured JSON response for REST endpoints. When an unhandled exception escapes a controller method, Spring MVC routes it to the ErrorController implementation, which produces a response like:
{
"timestamp": "2024-11-15T14:30:00Z",
"status": 500,
"error": "Internal Server Error",
"message": "Division by zero",
"path": "/api/calculate"
}
For custom exception handling, use @ControllerAdvice:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
}
}
Always return consistent error structures — do not let raw exception messages bubble up to clients in production.
The exclude attribute on @SpringBootApplication disables specific auto-configuration classes at startup. Use it when an auto-configured component conflicts with your setup:
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
SecurityAutoConfiguration.class
})
public class MyApplication { ... }
A common case is when you are integrating with a third-party library that ships its own auto-configuration but it collides with your manual setup. Excluding the auto-configuration class is cleaner than fighting the framework.
Note that excluding auto-configuration means you lose all its beans — if you exclude DataSourceAutoConfiguration, you must provide your own DataSource bean or the application fails to start. It is all-or-nothing per configuration class.
spring-boot-devtools is a starter that enables a suite of development-time conveniences:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
Automatic restart — when classpath files change, the application restarts. With most IDEs this is near-instant compared to a full JVM restart because it uses two classloaders (one for your code, one for Spring Boot jars).
LiveReload — automatic browser refresh when templates or static resources change.
Sensible defaults in dev — thymeleaf cache disabled, logging defaults tuned for development, better error pages.
Remote debugging — can connect to a running app over TCP for debugging in containerized environments.
DevTools is automatically disabled in production builds — do not ship it in your JAR.
Spring Boot Actuator exposes operational endpoints over HTTP for monitoring and management. After adding the starter, most endpoints are disabled by default for security — you explicitly expose what you need:
management.endpoints.web.exposure.include=health,info,metrics,beans,env
management.endpoint.health.show-details=when_authorized
Key built-in endpoints:
/actuator/health — application health, used by Kubernetes liveness/readiness probes
/actuator/metrics — Micrometer metrics (memory, HTTP requests, JVM stats)
/actuator/beans — all registered Spring beans (never expose in production)
/actuator/env — environment variables and config properties (contains secrets in non-production)
Securing actuator: map it behind your API gateway, restrict via Spring Security to authenticated users, or use management.endpoints.web.security.enabled=false to disable Spring Security integration entirely if you handle security at a different layer.
All three are specializations of @Component — Spring treats them identically for component scanning and bean registration. The difference is semantic:
@Component // Generic component — use for utilities, helpers, anything not fitting the others
public class StringUtils { ... }
@Service // Business logic layer — marks a class as holding business rules
public class OrderService { ... }
@Repository // Data access layer — marks a class as interacting with a database
public class UserRepository { ... }
Beyond semantics, @Repository has one concrete benefit: Spring automatically translates persistence exceptions (JDBC exceptions, JPA exceptions) into Spring's unchecked DataAccessException hierarchy via an ExceptionTranslator. This does not happen with @Component or @Service.
For documentation and team clarity, use the right stereotype. Do not put database logic in a @Service — put it in a @Repository.
Spring Boot loads configuration in a specific order, with later sources overriding earlier ones:
# 1. application.properties inside the JAR (defaults — never change this)
# 2. application-{profile}.properties on the classpath (e.g., application-prod.properties)
# 3. application-{profile}.yml on the classpath
# 4. OS environment variables
# 5. command-line arguments
Activate a profile with:
java -jar app.jar --spring.profiles.active=prod
# or
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
For secrets (passwords, API keys), never put them in source-controlled config files. Use environment variables at runtime:
spring.datasource.password=${DB_PASSWORD}
spring.security.oauth2.resourceserver.jwt.issuer-uri=${AUTH_ISSUER_URI}
Spring Boot's relaxed binding means DB_PASSWORD, db_password, and db_password all bind to spring.datasource.password.
A BeanDefinition is Spring's internal description of a single bean — its class, scope, constructor arguments, property values, and initialization/destruction callbacks. Spring Boot builds these from @Configuration classes, @ComponentScan results, and auto-configuration classes.
Auto-configuration contributes BeanDefinition entries through @Bean methods marked with conditional annotations. Only when all @Conditional* conditions pass does Spring register the bean definition into the BeanFactory.
@ConditionalOnMissingBean(DataSource.class) // Gate: only register if no existing bean
@Bean
public DataSource dataSource() {
return new HikariDataSource(...); // This BeanDefinition goes into the factory
}
At runtime, the BeanFactory iterates all registered BeanDefinition objects, instantiates each bean by calling its constructor (or factory method), injects dependencies, and returns the fully wired object. The order is determined by @AutoConfigureAfter and dependency relationships between beans.
@Configuration marks a class as a source of bean definitions. Spring creates a CGLIB proxy around it to ensure bean methods are intercepted — if you call dataSource() from another bean method in a @Configuration class, Spring returns the registered bean, not a new object. This is the "full mode" of @Configuration.
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(); // Returns the SAME bean from the factory
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource ds) {
// Spring intercepts this and passes the registered DataSource bean, not a new one
return new JdbcTemplate(ds);
}
}
@ComponentScan does not define beans — it tells Spring where to look for classes annotated with @Component, @Service, @Repository, @Controller, and other stereotype annotations so they can be registered automatically. By default it scans the package of the annotated class and all sub-packages.
They work together: @ComponentScan finds components, @Configuration classes define explicit bean wiring logic.
SpringApplication.run() bootstraps a Spring Boot application in these steps:
1. Create the application context — instantiates an AnnotationConfigServletWebServerApplicationContext, which is a Spring ApplicationContext with a built-in web server factory.
2. Register the sources — the class passed to run() (annotated with @SpringBootApplication) becomes a Configuration source, along with any other config classes found by @ComponentScan.
3. Execute auto-configuration — EnableAutoConfiguration triggers the auto-configuration engine, which evaluates all conditional annotations against the classpath and registers auto-configuration beans.
4. Start the embedded server — EmbeddedServletContainerAutoConfiguration creates and starts the servlet container (Tomcat, Jetty, or Undertow) on the configured port.
5. Refresh the context — all bean definitions are instantiated, dependencies are injected, and the application is fully running.
Further Reading
- RESTful API Design — designing clean REST interfaces that work well with Spring Boot controllers
- Spring Framework Core — understanding
@Component,@Bean, and the container Spring Boot builds on - REST Controller and Request Mapping — hands-on endpoint mapping with Spring Boot
- Spring Security Fundamentals — authentication and authorization patterns
- Spring Boot Documentation — official reference documentation
- Auto-Configuration in Depth — how the conditional loading system actually works
- Spring Boot Actuator — production-ready monitoring and health endpoints
- Containerizing Spring Boot with Docker — packaging applications as Docker images
Conclusion
Spring Boot came about because setting up a Spring project used to take days — XML configs everywhere, version conflicts, hunting for which library versions actually worked together. Starters, auto-configuration, and embedded servers fixed those three problems specifically. The defaults cover most of what you need in production, so you only configure what is different about your project.
From here, the fastest path forward is spring-boot-starter-web plus one of the linked tutorials. If you want more depth, auto-configuration conditions or the actuator endpoints are worth running to ground in a real project.
Category
Related Posts
Spring Boot Build Tools: Maven & Gradle
Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.
Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow
Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.
JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests
Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.