Spring Boot
Fundamentals
Spring Boot is not a framework — it's an opinionated layer on top of Spring that makes production-ready applications the default. Understanding what it does for you (and what it hides) is the difference between someone who uses Spring Boot and someone who engineers with it.
- Spring Boot Architecture — What It Actually Is
- Auto-Configuration — The Magic Explained
- Starter Dependencies
- Embedded Servers
- Configuration — Properties & YAML
- Configuration Hierarchy & Precedence
- @ConfigurationProperties — Type-Safe Config
- Profiles — Multi-Environment Management
- Spring Boot Startup Sequence
- Logging System
- DevTools & Actuator
Spring Boot Architecture — What It Actually Is
Before you write a single line of Spring Boot code, you need to understand what it actually is — because most engineers get this wrong. They think Spring Boot is a separate framework. It is not. Spring Boot is an opinionated assembly layer on top of the Spring Framework.
Think of it this way: the Spring Framework gives you all the tools — Dependency Injection, AOP, Transaction Management, MVC. But using raw Spring requires enormous amounts of XML configuration or Java configuration just to get a web server running. Spring Boot solves this by making sensible default decisions for you.
Spring Boot's job is to look at your classpath, your configuration, and your environment — and automatically wire together a working Spring application. It does NOT add new features. It adds opinionated defaults and auto-assembly.
The Layer Stack
Understanding where Spring Boot sits in the stack clarifies everything:
What Spring Boot Does At Startup
When your application starts, Spring Boot performs a precise sequence of actions. This matters because production issues — slow startups, missing beans, wrong configurations — all trace back to this sequence.
Bootstrap Phase
Spring Boot loads SpringApplication, determines the application type (SERVLET, REACTIVE, NONE), loads SpringApplicationRunListeners, and prepares the environment (reads system properties, environment variables, and application.properties).
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
// SpringApplication.run() starts the entire bootstrap process
SpringApplication app = new SpringApplication(MyApp.class);
app.run(args);
// 1. Creates SpringApplication instance
// 2. Determines WebApplicationType
// 3. Loads ApplicationContextInitializers
// 4. Loads ApplicationListeners
// 5. Prepares environment
// 6. Creates ApplicationContext
// 7. Refreshes context (beans, auto-config)
// 8. Calls runners (CommandLineRunner, ApplicationRunner)
}
}
Auto-Configuration Phase
Spring Boot scans META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Spring Boot 3.x) or spring.factories (older versions) and conditionally loads hundreds of auto-configuration classes based on what's on your classpath.
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // only if DataSource is on classpath
@ConditionalOnMissingBean(DataSource.class) // only if YOU haven't defined one
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
// Creates a HikariCP connection pool by default
return properties.initializeDataSourceBuilder().build();
}
}
Context Refresh Phase
This is the heaviest phase. Spring creates all beans defined by your code AND auto-configuration, resolves dependencies, initializes Hibernate/JPA if present, starts Tomcat/Jetty/Undertow, and registers all HTTP endpoints with the DispatcherServlet.
Slow startup is almost always in this phase. Common culprits: Hibernate schema validation, eager connection pool initialization, and scanning too broad a package tree. Always time your startup with --debug to see which beans take longest.
Ready Phase
After all beans are initialized, Spring Boot calls CommandLineRunner and ApplicationRunner implementations in order, then publishes ApplicationReadyEvent. Only now is the application accepting traffic.
@Component
@Order(1)
public class DatabaseSeeder implements CommandLineRunner {
@Override
public void run(String... args) {
// Runs after ALL beans are ready
// Perfect for: data seeding, connection validation, warm-up queries
log.info("Database seeded successfully");
}
}
@Component
public class CacheWarmer implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// ApplicationRunner gets parsed args — better for flag-driven startup
if (args.containsOption("warm-cache")) {
warmUpCache();
}
}
}
The @SpringBootApplication Annotation Unpacked
This single annotation does the work of three annotations. Knowing what each one does lets you remove the ones you don't need (important for testing and modular apps).
// @SpringBootApplication is equivalent to:
@SpringBootConfiguration // = @Configuration, marks this as a config class
@EnableAutoConfiguration // enables the auto-config mechanism
@ComponentScan // scans this package and all sub-packages for @Component, @Service, etc.
public class MyApp { }
// Practical implication: your main class package is the root scan boundary.
// ALL Spring-managed classes must be in this package or a sub-package.
// This is why you NEVER put @SpringBootApplication in the default package.
Placing @SpringBootApplication in com.company will scan EVERYTHING under com.company — including test utilities, internal libraries, and unrelated modules if they're in the classpath. This causes bean conflicts and massively slows startup. Structure your packages intentionally. Prefer com.company.myapp as the root.
Auto-Configuration — The Magic Explained
Auto-configuration is both Spring Boot's greatest feature and its most misunderstood one. Engineers who understand it write lean, production-ready applications. Engineers who don't write bloated apps full of unnecessary beans and configuration they didn't mean to load.
Auto-configuration is a collection of @Configuration classes that Spring Boot ships inside its JARs. Each one is guarded by conditions. They only activate when specific conditions are true — a class is on the classpath, a property is set, or you haven't provided your own bean. Your app tells Spring Boot what you have; Spring Boot figures out what to configure.
How It Works Internally
The mechanism involves three layers working together:
1. Registration
All auto-configuration classes are listed in META-INF/spring/…AutoConfiguration.imports. Spring Boot reads this file on startup to discover every auto-configuration candidate.
2. Condition Evaluation
@Conditional annotations on each class are evaluated. If all conditions pass, the configuration class is loaded. If any condition fails, the entire class is skipped — zero overhead.
3. Bean Creation
Surviving auto-configuration classes register their beans into the ApplicationContext just like your own @Configuration classes. Your beans take precedence via @ConditionalOnMissingBean.
The Condition Annotations
These are the building blocks of all auto-configuration. Memorize them — they're also extremely useful in your own code.
// CLASS CONDITIONS — does a class exist on the classpath?
@ConditionalOnClass(DataSource.class) // activate if DataSource.class exists
@ConditionalOnMissingClass("com.example.Foo") // activate if this class does NOT exist
// BEAN CONDITIONS — does a bean exist in the context?
@ConditionalOnBean(DataSource.class) // activate if a DataSource bean exists
@ConditionalOnMissingBean(DataSource.class) // activate if NO DataSource bean exists (most common)
@ConditionalOnSingleCandidate(DataSource.class) // activate if exactly ONE DataSource bean exists
// PROPERTY CONDITIONS — is a property set?
@ConditionalOnProperty(
name = "myapp.feature.enabled",
havingValue = "true",
matchIfMissing = false // if property absent, condition = false
)
// RESOURCE CONDITIONS — does a file exist?
@ConditionalOnResource(resources = "classpath:hibernate.cfg.xml")
// WEB CONDITIONS
@ConditionalOnWebApplication // activate in servlet-based web app
@ConditionalOnNotWebApplication // activate in non-web app (batch jobs, CLIs)
// EXPRESSION CONDITIONS — SpEL expression
@ConditionalOnExpression("${myapp.enabled:true} && ${myapp.version:1} > 2")
Writing Your Own Auto-Configuration
This is a critical skill for building internal libraries that auto-configure themselves when added as a dependency.
// 1. Write the auto-configuration class
@AutoConfiguration // marks as auto-config (Spring Boot 2.7+)
@ConditionalOnClass(AuditService.class) // only if our library is present
@ConditionalOnProperty(
prefix = "mycompany.audit",
name = "enabled",
matchIfMissing = true // enabled by default
)
@EnableConfigurationProperties(AuditProperties.class)
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean // user can override by defining their own
public AuditService auditService(AuditProperties props) {
return new DefaultAuditService(props.getLevel(), props.getDestination());
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(AuditService.class)
public AuditInterceptor auditInterceptor(AuditService service) {
return new AuditInterceptor(service);
}
}
// 2. Register it in your library JAR:
// src/main/resources/META-INF/spring/
// org.springframework.boot.autoconfigure.AutoConfiguration.imports
//
// Contents of that file:
// com.mycompany.audit.AuditAutoConfiguration
// 3. Any app that adds your library as a dependency gets audit features automatically.
Debugging Auto-Configuration
When something isn't working, the first tool you reach for is the auto-configuration report. It tells you exactly which configurations loaded, which didn't, and why.
# Run with --debug to print the full auto-configuration report
java -jar myapp.jar --debug
# Or set in application.properties:
# debug=true
# Output includes:
# Positive matches (what loaded and why)
# Negative matches (what was skipped and why)
# Exclusions (what you explicitly excluded)
# Unconditional classes (always loaded)
# With spring-boot-starter-actuator added and endpoint exposed:
curl http://localhost:8080/actuator/conditions
# Returns JSON showing every condition evaluation:
# {
# "positiveMatches": {
# "DataSourceAutoConfiguration": [
# { "condition": "OnClassCondition",
# "message": "@ConditionalOnClass found required class 'javax.sql.DataSource'" }
# ]
# },
# "negativeMatches": {
# "MongoAutoConfiguration": {
# "notMatched": [
# { "condition": "OnClassCondition",
# "message": "@ConditionalOnClass did not find required class 'com.mongodb.MongoClient'" }
# ]
# }
# }
# }
The condition report is one of the most useful debugging tools in Spring Boot. "Positive matches" = loaded. "Negative matches" = skipped. "Exclusions" = manually excluded. Always check negatives first when a feature isn't activating as expected.
// Exclude specific auto-configurations you don't want
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class, // no database
SecurityAutoConfiguration.class, // handle security manually
JmxAutoConfiguration.class // no JMX
})
public class MyApp { }
// Or via properties (useful for test environments):
// spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
RedisAutoConfiguration creates a default RedisTemplate. If you later define your own @Bean RedisTemplate, what happens?@ConditionalOnMissingBean. This is the fundamental escape hatch: define your own bean to override any auto-configured default. Your configuration always wins.
Starter Dependencies
Starters are dependency descriptors — Maven/Gradle POMs that group together all the libraries you need for a specific capability. They're the reason you don't need to hunt down which version of Hibernate works with which version of Spring Data JPA.
A starter contains: (1) a curated set of versioned dependency declarations, (2) usually a corresponding auto-configuration module, and (3) optionally a bill-of-materials (BOM) for transitive dependency management. The starter itself has zero code. It's a POM file.
Most Important Starters
// In Maven pom.xml — these are the starters you'll use constantly:
// WEB — Tomcat + Spring MVC + Jackson
// spring-boot-starter-web
// DATA — Spring Data JPA + Hibernate + HikariCP
// spring-boot-starter-data-jpa
// SECURITY — Spring Security
// spring-boot-starter-security
// TESTING — JUnit 5 + Mockito + MockMvc + AssertJ
// spring-boot-starter-test
// VALIDATION — Hibernate Validator (JSR-380)
// spring-boot-starter-validation
// ACTUATOR — production monitoring endpoints
// spring-boot-starter-actuator
// CACHE — Spring Cache abstraction
// spring-boot-starter-cache
// REDIS — Spring Data Redis + Lettuce client
// spring-boot-starter-data-redis
// KAFKA — Spring for Apache Kafka
// spring-kafka
// MAIL — JavaMail + Spring Mail
// spring-boot-starter-mail
// ASYNC — WebFlux reactive stack
// spring-boot-starter-webflux
What spring-boot-starter-web Pulls In
Running mvn dependency:tree on spring-boot-starter-web reveals the full picture:
spring-boot-starter-web
├── spring-boot-starter (core Spring Boot)
│ ├── spring-boot
│ ├── spring-boot-autoconfigure
│ └── spring-boot-starter-logging (Logback + SLF4J)
├── spring-boot-starter-json
│ └── jackson-databind (JSON serialization)
├── spring-boot-starter-tomcat
│ └── tomcat-embed-core (embedded web server)
└── spring-web + spring-webmvc (Spring MVC)
Swapping Components
Starters are designed to be composable. You can swap any component by excluding it and adding an alternative:
// In 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-undertow</artifactId>
// </dependency>
// Why Undertow? Non-blocking I/O, lower memory, better performance
// at high concurrency — common choice for microservices.
Spring Boot's dependency management BOM (spring-boot-dependencies) contains hundreds of tested, compatible version combinations. Overriding a version number breaks this guarantee and can cause subtle runtime failures that are very hard to debug. Only override when you have a specific security or bug fix reason — and test thoroughly.
Embedded Servers
Traditional Java web applications were deployed into an external server — you ran Tomcat separately, then deployed a WAR file to it. Spring Boot inverts this: the server runs inside your application. This is a fundamental architectural shift with real production consequences.
Traditional WAR Deployment
Tomcat/JBoss/WebLogic runs as an OS process. You deploy your WAR into its webapps/ directory. The server manages the lifecycle. Multiple WARs share the same JVM.
Embedded JAR Deployment
Your application IS the server. java -jar myapp.jar starts everything. One JVM, one application. Trivial to Dockerize. Zero external dependencies at runtime.
Available Embedded Servers
Tomcat is the default embedded server for servlet-based applications. Battle-tested, widely supported, excellent documentation. Use it unless you have a specific reason not to.
// application.properties
// server.port=8080
// server.tomcat.max-threads=200 # max worker threads
// server.tomcat.min-spare-threads=10 # always-alive threads
// server.tomcat.connection-timeout=20s # time to wait for connection
// server.tomcat.accept-count=100 # queue size when threads full
// server.tomcat.max-connections=8192 # max simultaneous connections
Jetty uses a non-blocking I/O model with a thread pool that's better at handling many concurrent idle connections. Good choice for WebSocket-heavy applications.
// Exclude tomcat, add jetty in pom.xml
// server.jetty.threads.max=200
// server.jetty.threads.min=8
// server.jetty.connection-idle-timeout=60000ms
Undertow (by Red Hat/JBoss) is highly efficient with low memory overhead. Uses non-blocking I/O with a small set of worker threads. Often the fastest in benchmarks for high-concurrency workloads.
// Exclude tomcat, add undertow in pom.xml
// server.undertow.threads.io=4 # I/O threads (usually CPU count)
// server.undertow.threads.worker=32 # worker threads
// server.undertow.buffer-size=1024 # buffer size per connection
Netty is used when you switch to the reactive stack (spring-boot-starter-webflux). It's fully non-blocking. Not compatible with Spring MVC — it's a different programming model entirely.
// Replace spring-boot-starter-web with spring-boot-starter-webflux
// Netty is pulled in automatically
@RestController
public class ReactiveController {
@GetMapping("/items")
public Flux<Item> getItems() {
// Returns a reactive stream — never blocks a thread
return itemRepository.findAll();
}
}
Customizing the Embedded Server Programmatically
@Component
public class TomcatCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.setPort(8443);
// Configure SSL/TLS programmatically
Ssl ssl = new Ssl();
ssl.setKeyStore("classpath:keystore.p12");
ssl.setKeyStorePassword(System.getenv("KEYSTORE_PASSWORD"));
ssl.setKeyStoreType("PKCS12");
factory.setSsl(ssl);
// Add Tomcat Valve (for request logging, access control)
factory.addEngineValves(new AccessLogValve());
// Increase connection timeout for heavy file uploads
factory.addConnectorCustomizers(connector ->
connector.setProperty("connectionTimeout", "60000")
);
}
}
For CPU-bound workloads: set max threads to 2× CPU cores. For I/O-bound workloads (waiting on DB, external APIs): 10–20× CPU cores is reasonable. Always load test with realistic traffic — thread pool sizing is empirical, not theoretical.
Configuration — Properties & YAML
Spring Boot has the most powerful configuration system in the Java ecosystem. Understanding it fully separates engineers who hard-code environment differences from those who build truly portable, environment-agnostic applications.
Two Formats, One System
Spring Boot supports both .properties and .yml formats. They're equivalent in capability — choose one and be consistent within a project.
# Server
server.port=8080
server.servlet.context-path=/api
# Database
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=postgres
spring.datasource.password=secret
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Connection pool (HikariCP)
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
# Custom property
myapp.feature.rate-limit.enabled=true
myapp.feature.rate-limit.requests-per-second=100
server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: postgres
password: secret
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
jpa:
show-sql: true
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
myapp:
feature:
rate-limit:
enabled: true
requests-per-second: 100
Use YAML for new projects. The hierarchical structure makes deeply-nested config readable. The main gotcha: YAML is whitespace-sensitive. A tab instead of spaces will silently break your config. Use a linter.
Use properties when you need simplicity, or when the config will be injected via environment variables in CI/CD systems (property format is easier to override line-by-line).
Never mix both for the same property key — .properties takes precedence over .yml when both exist for the same profile.
Reading Configuration in Code
There are three ways to access configuration values. Each has a different use case.
// ── Method 1: @Value (simple, one-off values) ──────────────────────────
@Service
public class EmailService {
@Value("${myapp.email.from}")
private String fromAddress;
@Value("${myapp.email.max-retries:3}") // :3 is the default value
private int maxRetries;
@Value("${myapp.email.recipients}") // comma-separated → List
private List<String> recipients;
}
// ── Method 2: Environment (programmatic access) ────────────────────────
@Service
public class FeatureService {
@Autowired
private Environment env;
public boolean isFeatureEnabled(String featureName) {
return env.getProperty("myapp.features." + featureName, Boolean.class, false);
}
}
// ── Method 3: @ConfigurationProperties (preferred for groups) ─────────
// Covered in depth in the next topic — this is the production standard.
Multi-Document YAML
YAML supports multiple documents in one file using --- as a separator. This is how you can define profile-specific config in a single file:
# Default config (all environments)
server:
port: 8080
spring:
application:
name: order-service
---
# Dev profile override
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:testdb
---
# Production profile override
spring:
config:
activate:
on-profile: prod
datasource:
url: ${DATABASE_URL} # from environment variable
Passwords, API keys, and connection strings should never be in application.properties or application.yml if those files are committed to version control. Use environment variables (${MY_SECRET}), a secrets manager (AWS Secrets Manager, Vault), or Spring Cloud Config Server for sensitive values. This is a compliance requirement at most companies.
Configuration Hierarchy & Precedence
Spring Boot supports over 17 configuration sources, all merged together with a defined priority order. Understanding this hierarchy is critical for production operations — it determines which value "wins" when the same property is set in multiple places.
When the same property appears in multiple sources, the source with higher priority wins. This is intentional — it lets you override config without touching code. Environment variables override application.properties which overrides defaults.
Production Implications
In containerized environments, environment variables (priority 12) and command-line args (priority 14) are the standard mechanisms for injecting configuration — because you don't want to rebuild the image for each environment.
# Docker run — inject via environment variables
docker run \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://prod-db:5432/orders \
-e SPRING_DATASOURCE_PASSWORD=secret \
-e MYAPP_FEATURE_RATELIMIT_ENABLED=true \
myapp:latest
# Note: Spring Boot maps env vars to properties automatically:
# SPRING_DATASOURCE_URL → spring.datasource.url
# MY_APP_API_KEY → my.app.api-key
# Dots → underscores, uppercase → lowercase (relaxed binding)
# Override any property at runtime via command-line arg
java -jar myapp.jar --server.port=9090 --spring.profiles.active=prod
# Override via system property (JVM flag)
java -Dserver.port=9090 -Dspring.profiles.active=prod -jar myapp.jar
# Place application.properties NEXT TO the JAR to override without rebuild
# /deploy/
# myapp.jar
# application.properties ← overrides what's baked into the JAR
When a property has an unexpected value in production, check the Actuator /env endpoint (if enabled) — it shows every property source and which one is winning. Always protect this endpoint with authentication in production.
curl http://localhost:8080/actuator/env/spring.datasource.url
# Returns the value AND which source it came from:
# {
# "property": {
# "source": "systemEnvironment",
# "value": "jdbc:postgresql://prod-db:5432/orders"
# }
# }
@ConfigurationProperties — Type-Safe Config
@Value works for one-off values but falls apart when you have a group of related properties. @ConfigurationProperties binds an entire prefix of properties to a Java class — giving you type safety, validation, IDE autocompletion, and testability.
This is the production standard for configuration. Any serious Spring Boot project uses @ConfigurationProperties for custom config.
// 1. Define the properties class
@ConfigurationProperties(prefix = "myapp.mail")
@Validated // enables JSR-380 validation on startup
public class MailProperties {
@NotBlank
private String host;
@Min(1) @Max(65535)
private int port = 587; // default value
@NotBlank
private String username;
@NotBlank
private String password;
private boolean startTlsEnabled = true;
@DurationUnit(ChronoUnit.SECONDS)
private Duration connectionTimeout = Duration.ofSeconds(30);
private List<String> adminRecipients = new ArrayList<>();
// Nested configuration class
private Retry retry = new Retry();
public static class Retry {
private int maxAttempts = 3;
private Duration delay = Duration.ofSeconds(5);
// getters/setters...
}
// Standard getters and setters (or use Lombok @Data)
}
// 2. Register it
@Configuration
@EnableConfigurationProperties(MailProperties.class) // explicit registration
public class AppConfig { }
// OR just add @ConfigurationPropertiesScan to your main class
// 3. Use it
@Service
public class EmailService {
private final MailProperties mailProps;
public EmailService(MailProperties mailProps) {
this.mailProps = mailProps;
}
public void send(String to, String subject, String body) {
// Use mailProps.getHost(), mailProps.getPort(), etc.
}
}
// 4. application.yml binds automatically:
// myapp:
// mail:
// host: smtp.mycompany.com
// port: 587
// username: noreply@mycompany.com
// password: ${MAIL_PASSWORD}
// start-tls-enabled: true
// connection-timeout: 30s
// admin-recipients:
// - ops@mycompany.com
// - alerts@mycompany.com
// retry:
// max-attempts: 3
// delay: 5s
Relaxed Binding
Spring Boot's relaxed binding means these all map to the same property myapp.mail.startTlsEnabled:
myapp.mail.startTlsEnabled=true # camelCase
myapp.mail.start-tls-enabled=true # kebab-case (recommended for .yml)
myapp.mail.start_tls_enabled=true # underscore
MYAPP_MAIL_STARTTLSENABLED=true # env var (uppercase, underscores)
IDE Support — Configuration Metadata
Add the annotation processor to get autocompletion in application.yml for your custom properties:
// In pom.xml dependencies:
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-configuration-processor</artifactId>
// <optional>true</optional>
// </dependency>
// This generates META-INF/spring-configuration-metadata.json
// IntelliJ and VS Code read this file to provide:
// - Property name completion
// - Type information
// - Default value hints
// - Javadoc tooltips
Use @Value for a single, simple property injection — especially in one-off utilities or small config classes. Use @ConfigurationProperties for anything with 2+ related properties, when you need validation, when you need IDE support, or when properties are shared across multiple beans. In production codebases, @ConfigurationProperties dominates.
@ConfigurationProperties(prefix="myapp.cache") class with a private Duration ttl field. In application.yml, you set myapp.cache.ttl: 5m. What value does ttl get?5s, 5m, 5h, 5d) and ISO-8601 (PT5M) for Duration fields. This relaxed binding is one of the big advantages of @ConfigurationProperties over raw @Value.
Profiles — Multi-Environment Management
Profiles are Spring's mechanism for environment-specific configuration. Every real project needs to run differently in development, testing, staging, and production. Profiles let you express those differences cleanly without conditional logic scattered through your code.
Activating Profiles
# Via command-line (deployment standard)
java -jar myapp.jar --spring.profiles.active=prod
# Via environment variable (Docker/K8s standard)
export SPRING_PROFILES_ACTIVE=prod
# Via application.properties (dev default — set this in your repo)
# spring.profiles.active=dev
# Multiple profiles (comma-separated, all activate simultaneously)
java -jar myapp.jar --spring.profiles.active=prod,metrics,feature-flags
Profile-Specific Config Files
Spring Boot automatically loads application-{profile}.yml on top of the base application.yml. Profile-specific values override base values.
src/main/resources/
application.yml # base config (all environments)
application-dev.yml # dev overrides
application-test.yml # test overrides
application-staging.yml # staging overrides
application-prod.yml # production overrides
spring:
datasource:
url: jdbc:h2:mem:devdb;MODE=PostgreSQL
username: sa
password: ""
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop # recreate schema on startup
logging:
level:
com.mycompany: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql: TRACE # shows bind parameters
myapp:
feature:
email-sending: false # don't actually send emails in dev
spring:
datasource:
url: ${DATABASE_URL}
username: ${DATABASE_USER}
password: ${DATABASE_PASSWORD}
hikari:
maximum-pool-size: 50
jpa:
show-sql: false
hibernate:
ddl-auto: validate # NEVER create-drop in prod
logging:
level:
root: WARN
com.mycompany: INFO
server:
tomcat:
max-threads: 400
Profile-Specific Beans
Beyond configuration files, you can conditionally register entire beans based on the active profile:
// Use a fake payment provider in dev/test, real one in prod
public interface PaymentGateway {
PaymentResult charge(PaymentRequest request);
}
@Service
@Profile("!prod") // active for all profiles EXCEPT prod
public class MockPaymentGateway implements PaymentGateway {
@Override
public PaymentResult charge(PaymentRequest request) {
log.info("MOCK: Charging {} for ${}", request.getCustomerId(), request.getAmount());
return PaymentResult.success("mock-txn-" + UUID.randomUUID());
}
}
@Service
@Profile("prod")
public class StripePaymentGateway implements PaymentGateway {
@Override
public PaymentResult charge(PaymentRequest request) {
// actual Stripe API call
return stripeClient.charge(request);
}
}
// OrderService gets whichever bean is active — no conditional logic needed
@Service
public class OrderService {
private final PaymentGateway paymentGateway; // injected correctly by profile
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
Profile Groups (Spring Boot 2.4+)
Profile groups let you activate multiple profiles with a single name — very useful for complex multi-environment setups:
spring:
profiles:
group:
prod: prod-db, prod-cache, prod-monitoring, prod-security
dev: dev-db, dev-stubs, dev-debug
# Activating "prod" now activates all four sub-profiles:
# java -jar app.jar --spring.profiles.active=prod
Setting spring.jpa.hibernate.ddl-auto=create-drop or create in your base application.yml and forgetting to override it in application-prod.yml will wipe your production database on startup. This has happened in real companies. Always set ddl-auto=validate or none in production — use Flyway or Liquibase for schema migrations.
application.yml sets server.port: 8080. Your application-prod.yml sets server.port: 80. You run java -jar app.jar --server.port=443 --spring.profiles.active=prod. What port does the server start on?Spring Boot Startup Sequence
Knowing the exact startup sequence helps you diagnose startup failures, optimize startup time, and place your initialization code in the right phase. Guessing which lifecycle callback to use is a common source of bugs in enterprise Spring Boot applications.
Click any step to expand details
Bean Lifecycle vs Application Lifecycle
Don't confuse the two. Bean lifecycle is about a single bean being created. Application lifecycle is about the whole app starting.
@Service
public class MyService implements InitializingBean, DisposableBean {
// Option 1: @PostConstruct (JSR-250 — recommended)
// Runs after DI is complete, before bean is put into use
@PostConstruct
public void init() {
log.info("Bean initialized — dependencies are injected and ready");
// Load lookup tables, validate connections, warm up caches
}
// Option 2: InitializingBean.afterPropertiesSet()
// Called at the same time as @PostConstruct (just before)
@Override
public void afterPropertiesSet() {
// Framework-level init logic — prefer @PostConstruct in your code
}
// Option 3: @Bean(initMethod = "start")
// For third-party beans you can't annotate
// SHUTDOWN ─────────────────────────────────────────────────
@PreDestroy
public void cleanup() {
// Runs when the Spring context closes (app shutdown)
// Close connections, flush buffers, release resources
}
@Override
public void destroy() {
// DisposableBean equivalent — called at same time as @PreDestroy
}
}
// ── Application lifecycle (broader scope) ──────────────────────
@Component
@Order(1)
public class AppStartupRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// Runs after ALL beans are ready (step 7 in startup sequence)
// Better for: cross-bean coordination, data loading, health checks
}
}
Startup Time Optimization
Slow startups hurt developer experience and increase pod startup latency in Kubernetes. Common causes and fixes: (1) Broad component scanning — narrow your @ComponentScan base package. (2) Hibernate schema validation — switch to ddl-auto=none and use Flyway. (3) Connection pool initialization — use lazy initialization for non-critical pools. (4) Spring Boot Lazy Initialization — set spring.main.lazy-initialization=true (beans created on first use, not startup). (5) GraalVM Native Image — compile to native binary for sub-second startup (Spring Boot 3.x).
Logging System
Logging is one of the most underengineered aspects of Spring Boot applications. Proper logging practices separate systems that are debuggable in production from ones that leave you flying blind when something goes wrong at 2 AM.
The Logging Stack
Spring Boot uses SLF4J as the logging facade and Logback as the default implementation. You write code against the SLF4J API — the underlying implementation is swappable.
SLF4J (Facade)
Simple Logging Facade for Java. Your code calls log.info(), log.error() etc. against this API. Implementation-agnostic.
Logback (Default)
Fast, feature-rich implementation. Supports rolling file appenders, async logging, conditional logging, and structured log output.
Log4j2 (Alternative)
Higher throughput than Logback for async logging. Popular in high-performance services. Swap by excluding Logback and adding log4j2 starter.
Writing Log Statements
// Always use a static final logger — one per class
@Service
public class OrderService {
// SLF4J logger — Lombok @Slf4j annotation generates this
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
// Or with Lombok: just add @Slf4j to the class
public Order createOrder(CreateOrderRequest request) {
// ── GOOD: parameterized messages (no string concatenation) ──
log.info("Creating order for customer={} items={}",
request.getCustomerId(), request.getItemCount());
// ── NEVER do this — string concat happens even if DEBUG is off ──
// log.debug("Order: " + request.toString()); // BAD
// ── DEBUG for detailed diagnostic information ──
log.debug("Order request details: {}", request); // toString() only called if DEBUG is on
try {
Order order = processOrder(request);
log.info("Order created successfully orderId={} total={}",
order.getId(), order.getTotal());
return order;
} catch (InsufficientStockException e) {
// Warn-level for expected, recoverable business issues
log.warn("Insufficient stock for order customerId={} productId={}",
request.getCustomerId(), e.getProductId());
throw e;
} catch (Exception e) {
// Error-level with full stack trace for unexpected failures
log.error("Failed to create order for customerId={}",
request.getCustomerId(), e); // pass exception as last arg for stack trace
throw new OrderProcessingException("Order creation failed", e);
}
}
}
Log Levels Configuration
logging:
level:
root: WARN # global minimum level
com.mycompany: INFO # your app at INFO
com.mycompany.service: DEBUG # specific package more verbose
org.hibernate.SQL: DEBUG # see generated SQL
org.hibernate.type.descriptor.sql: TRACE # see SQL bind parameters
org.springframework.web: DEBUG # see request mapping
# Log file output (optional)
file:
name: /var/log/myapp/application.log
pattern:
# Console pattern with color
console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n"
# File pattern without color codes
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p --- [%t] %-40.40logger{39} : %m%n"
Structured Logging (Production Standard)
In production, plain text logs are hard to search and aggregate. Structured JSON logging integrates with log aggregation systems like ELK Stack (Elasticsearch, Logstash, Kibana) and Splunk.
// pom.xml dependency:
// <dependency>
// <groupId>net.logstash.logback</groupId>
// <artifactId>logstash-logback-encoder</artifactId>
// <version>7.4</version>
// </dependency>
// logback-spring.xml (place in src/main/resources/)
// <configuration>
// <springProfile name="prod">
// <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
// <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
// </appender>
// <root level="INFO">
// <appender-ref ref="JSON" />
// </root>
// </springProfile>
// </configuration>
// Each log line becomes JSON like:
// {
// "@timestamp": "2024-01-15T10:30:00.000Z",
// "level": "INFO",
// "logger_name": "com.mycompany.OrderService",
// "message": "Creating order for customer=123 items=3",
// "thread_name": "http-nio-8080-exec-1",
// "traceId": "abc123" ← from Spring Cloud Sleuth / Micrometer Tracing
// }
MDC — Correlation IDs
Mapped Diagnostic Context (MDC) lets you attach key-value pairs to every log line in a thread, making it possible to trace all logs from a single request.
@Component
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
// Extract or generate correlation ID
String correlationId = Optional
.ofNullable(request.getHeader("X-Correlation-Id"))
.orElse(UUID.randomUUID().toString());
try {
// Put into MDC — now appears in EVERY log line for this request
MDC.put("correlationId", correlationId);
MDC.put("userId", extractUserId(request));
MDC.put("requestPath", request.getRequestURI());
((HttpServletResponse) res).setHeader("X-Correlation-Id", correlationId);
chain.doFilter(req, res);
} finally {
MDC.clear(); // CRITICAL: clear MDC after each request (thread pool reuse)
}
}
}
Never log sensitive data (passwords, tokens, PII, card numbers) — this is a compliance and security violation. Never log inside tight loops — use aggregated metrics instead. Never use System.out.println in production code. Never catch-and-swallow exceptions silently without a log statement. Never concatenate strings for log messages — always use parameterized logging.
DevTools & Actuator
Two tools that every Spring Boot engineer should master: DevTools for a faster development loop, and Actuator for production observability. Used correctly, they drastically improve both productivity and operational confidence.
Spring DevTools — Fast Development Loop
Add spring-boot-devtools as a dependency and get three major productivity improvements automatically:
Automatic Restart
When class files change, DevTools restarts the application using a fast incremental classloader — typically 2–5× faster than a cold start. The embedded server stays up between restarts.
LiveReload
Automatically refreshes the browser when static resources (HTML, CSS, JS) change. Works with the LiveReload browser extension.
Developer-Friendly Defaults
Disables template caching (Thymeleaf, Freemarker), enables H2 console, enables DEBUG logging for web — automatically, without configuration.
// pom.xml:
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-devtools</artifactId>
// <optional>true</optional> // IMPORTANT: optional = not included in final JAR
// </dependency>
// DevTools is automatically DISABLED in production:
// - When running from a JAR (java -jar)
// - When a "production" classloader is detected
// - It's safe to leave in your pom.xml
// Configure restart triggers and exclusions:
// spring.devtools.restart.exclude=static/**,public/**
// spring.devtools.restart.additional-paths=src/main/webapp
// spring.devtools.livereload.enabled=true
In IntelliJ: Enable "Build project automatically" in Settings → Compiler. Then enable "Allow auto-make to start even if developed application is currently running" in Registry (Cmd+Shift+A → Registry). This ensures class files are recompiled on save, triggering DevTools restart automatically.
Spring Boot Actuator — Production Observability
Actuator exposes operational information about your running application via HTTP or JMX endpoints. It's your first line of defense for production debugging.
# application.yml
management:
endpoints:
web:
exposure:
include: health, info, metrics, env, beans, conditions, loggers
# include: "*" — exposes ALL (only in controlled environments)
endpoint:
health:
show-details: always # show component health breakdown
show-components: always
env:
show-values: never # IMPORTANT: never expose property values in prod
server:
port: 8081 # run actuator on separate port (recommended)
info:
env:
enabled: true # allow info.* properties in app config
# HEALTH — application health status (used by K8s liveness/readiness probes)
GET /actuator/health
# {"status":"UP","components":{"db":{"status":"UP"},"diskSpace":{"status":"UP"}}}
# METRICS — application metrics (thread count, memory, GC, HTTP request stats)
GET /actuator/metrics
GET /actuator/metrics/http.server.requests
GET /actuator/metrics/jvm.memory.used?tag=area:heap
# INFO — application info (version, git commit, build time)
GET /actuator/info
# ENV — all configuration properties and their sources
GET /actuator/env
GET /actuator/env/spring.datasource.url
# BEANS — every bean in the context (debugging DI issues)
GET /actuator/beans
# CONDITIONS — auto-configuration report
GET /actuator/conditions
# LOGGERS — view and CHANGE log levels at runtime (no restart needed!)
GET /actuator/loggers/com.mycompany
POST /actuator/loggers/com.mycompany
# body: {"configuredLevel": "DEBUG"}
# HTTPTRACE / HTTPEXCHANGES — recent HTTP request/response history
GET /actuator/httpexchanges
# THREADDUMP — current JVM thread dump (diagnose deadlocks)
GET /actuator/threaddump
# HEAPDUMP — download a heap dump (diagnose memory leaks)
GET /actuator/heapdump
# SHUTDOWN — gracefully stop the app (must explicitly enable)
POST /actuator/shutdown
Custom Health Indicators
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final ExternalApiClient apiClient;
public ExternalApiHealthIndicator(ExternalApiClient apiClient) {
this.apiClient = apiClient;
}
@Override
public Health health() {
try {
boolean isUp = apiClient.ping();
if (isUp) {
return Health.up()
.withDetail("url", apiClient.getBaseUrl())
.withDetail("responseTimeMs", apiClient.getLastResponseTime())
.build();
} else {
return Health.down()
.withDetail("reason", "External API did not respond to ping")
.build();
}
} catch (Exception e) {
return Health.down(e)
.withDetail("reason", "Exception during health check")
.build();
}
}
}
// Appears under: /actuator/health/externalApi
Custom Info Contributors
// In application.yml:
// info:
// app:
// name: Order Service
// version: @project.version@ ← Maven variable interpolation
// environment: ${spring.profiles.active}
// contact:
// team: backend-platform
// slack: "#backend-alerts"
// Enable build info (add to pom.xml build plugins):
// <plugin>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-maven-plugin</artifactId>
// <executions>
// <execution>
// <goals><goal>build-info</goal></goals>
// </execution>
// </executions>
// </plugin>
// Result: /actuator/info returns:
// { "app": { "name": "Order Service", "version": "1.2.3" },
// "build": { "artifact": "order-service", "version": "1.2.3",
// "time": "2024-01-15T10:30:00Z" } }
Securing Actuator Endpoints
The /actuator/env endpoint exposes all configuration properties. The /actuator/heapdump endpoint lets anyone download your heap (which contains passwords in memory). The /actuator/shutdown endpoint kills your application. These endpoints MUST be secured or kept off a separate internal port in production.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Public health endpoint (for load balancer checks)
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/info").permitAll()
// All other actuator endpoints require ADMIN role
.requestMatchers("/actuator/**").hasRole("ACTUATOR_ADMIN")
// Your API endpoints
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults()); // or JWT, or IP allowlist
return http.build();
}
}
// Better approach for production: run actuator on a separate port
// that's only accessible within your internal network (VPC, Kubernetes service)
// management.server.port=8081
// Then block port 8081 at the firewall/ingress level from public access
Kubernetes Health Probes with Actuator
# Spring Boot 2.3+ automatically exposes K8s-specific probes:
management:
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
# In Kubernetes deployment spec:
# livenessProbe:
# httpGet:
# path: /actuator/health/liveness
# port: 8081
# initialDelaySeconds: 30
# periodSeconds: 10
#
# readinessProbe:
# httpGet:
# path: /actuator/health/readiness
# port: 8081
# initialDelaySeconds: 10
# periodSeconds: 5
#
# Liveness: is the app alive? If DOWN, K8s restarts the pod.
# Readiness: is the app ready to receive traffic? If DOWN, K8s removes from load balancer.
initialDelaySeconds on the readiness probe, or better — use Spring Boot's AvailabilityChangeEvent to programmatically control readiness state during migration.
Production Pitfalls Checklist
Real issues that show up in production Spring Boot applications. Review this before every deployment.
Wipes your schema on every startup. Use validate or none. Manage schema with Flyway or Liquibase.
Exposes heap dumps, config values, shutdown endpoint. Secure with authentication or restrict to internal network.
Use environment variables or a secrets manager. Never commit passwords, API keys, or tokens.
Default pool of 10 connections throttles under load. Size appropriately: spring.datasource.hikari.maximum-pool-size.
K8s routes traffic to pods before they're ready. Use Actuator health endpoints as probe targets.
Causes slow startup and unexpected bean discovery. Always put it in a specific sub-package.
Causes performance overhead and extra restarts. Use <optional>true</optional> in pom.xml — DevTools auto-disables when running as a JAR.
Spring Boot logs meaningful warnings at startup about deprecated config, missing components, and misconfiguration. Always read the startup log.
Interview Questions
Spring Boot fundamentals are asked in virtually every Java backend interview. These are the real questions, with engineering-level answers.
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Each class is guarded by @Conditional annotations that check classpath, existing beans, and properties. If conditions pass, the configuration class registers beans. Your own beans take precedence via @ConditionalOnMissingBean.@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan. It marks the class as a configuration class, enables auto-configuration, and scans the current package (and sub-packages) for Spring-managed components.@Value injects a single property. @ConfigurationProperties binds a group of properties to a class with type safety, validation, IDE support, and relaxed binding. For anything beyond one or two properties, @ConfigurationProperties is the production standard.DataSourceAutoConfiguration was explicitly excluded. (3) A profile-specific configuration excludes the data source. Check the auto-configuration report (--debug) and the active profiles at startup.@Profile on beans for coarse-grained feature toggling (whole beans in/out). For fine-grained flags, use @ConfigurationProperties to bind a Map<String, Boolean> of feature flags, then inject and check at runtime. Advanced: use Spring Cloud's property refresh for runtime flag changes without restart.spring.main.lazy-initialization=true — beans created on first use. (3) Switch to ddl-auto=none and skip schema validation on startup. (4) Exclude unused auto-configurations. (5) Use Spring Boot Native (GraalVM) for sub-second startup. (6) Profile startup with -Dspring.jmx.enabled=false and measure each phase.${SECRET_NAME} without knowing the source.health (liveness/readiness for K8s), info (version/build info), metrics (for Prometheus scraping), loggers (for runtime log level changes). Never expose: heapdump, env (shows config values), shutdown on public ports. Security: run management on a separate port (8081) accessible only within the internal network, authenticate with Spring Security, and restrict sensitive endpoints to ADMIN roles.