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

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.

The Core Insight

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:

Spring Boot Layer Architecture
YOUR APPLICATION CODE
Spring Boot (Auto-Config + Starters)
Spring Framework (Core + MVC + Data + Security)
Java SE + JVM
Operating System + Hardware

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).

Main Entry Point
@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.

Auto-Config Example (simplified)
@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.

Performance Trap

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.

Startup Hooks
@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).

What @SpringBootApplication Expands To
// @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.
Production Mistake: Package Placement

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.

The Mental Model

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.

All Key @Conditional Annotations
// 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.

Custom Auto-Configuration
// 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.

Enable Debug Output
# 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)
Actuator Conditions Endpoint
# 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'" }
#       ]
#     }
#   }
# }
Reading the Condition Report

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.

Excluding Auto-Configurations
// 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
Quick Check
You add a Redis dependency to your project but don't configure any Redis beans. Spring Boot's RedisAutoConfiguration creates a default RedisTemplate. If you later define your own @Bean RedisTemplate, what happens?
Both beans exist and Spring throws a NoUniqueBeanDefinitionException
Your bean takes over — auto-config's bean is suppressed by @ConditionalOnMissingBean
Your bean is ignored because auto-config has higher priority
Spring Boot throws a startup error about duplicate beans
Correct. Every auto-configured bean is guarded by @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.

What a Starter Actually Contains

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

pom.xml — Key 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:

Dependency Tree (abbreviated)
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:

Replacing Tomcat with Undertow
// 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.
Don't Override Managed Versions Without Reason

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.

Tomcat Configuration
// 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.

Switch to Jetty
// 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.

Switch to Undertow
// 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.

Reactive Stack (Netty)
// 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

WebServerFactoryCustomizer
@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")
        );
    }
}
Production Sizing Rule of Thumb

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.

application.properties
# 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
application.yml
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
Recommendation

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.

Three Ways to Read Config
// ── 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:

Multi-Document application.yml
# 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
Never Commit Secrets to Config Files

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.

The Rule: Higher Number = Higher Priority

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.

Configuration Priority Order (highest → lowest)
14 Command-line arguments --server.port=9090
13 SPRING_APPLICATION_JSON (env var / system property)
12 OS Environment Variables SERVER_PORT=9090
11 Java System Properties -Dserver.port=9090
9 application-{profile}.properties/yml (outside JAR)
7 application.properties/yml (outside JAR)
5 application-{profile}.properties/yml (inside JAR)
3 application.properties/yml (inside JAR / classpath)
1 @SpringBootApplication 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 / Kubernetes Config Injection
# 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)
Runtime Overrides
# 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
Debugging Config Issues in Production

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.

Actuator Environment Endpoint
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.

Full @ConfigurationProperties Example
// 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:

Relaxed Binding Forms
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:

Enable Autocompletion (pom.xml)
// 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
@ConfigurationProperties vs @Value — When to Use Which

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.

Quick Check
You have a @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?
The string "5m" (Duration.parse doesn't handle this)
5 milliseconds
5 minutes (Spring Boot's DurationStyle auto-parses "5m" → PT5M)
A startup error — use ISO-8601 format PT5M instead
Correct. Spring Boot supports a simple duration format (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

Activating a Profile
# 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.

Profile Config File Structure
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
application-dev.yml
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
application-prod.yml
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:

@Profile on Beans
// 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:

Profile Groups in application.yml
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
Common Profile Mistake: ddl-auto

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.

Quick Check
Your base 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?
8080 — base config always wins
80 — profile config overrides base config
443 — command-line args have the highest priority
An error — conflicting port definitions
Correct. Command-line arguments (priority 14) beat profile-specific config files (priority 9), which beat base config files (priority 3). The hierarchy always resolves to one winner.

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.

Full Startup Sequence — Interactive Explorer
1
SpringApplication.run() called
2
Environment Prepared
3
ApplicationContext Created
4
Bean Definitions Loaded
5
Context Refresh — Bean Instantiation
6
ApplicationStartedEvent Fired
7
CommandLineRunner / ApplicationRunner Called
8
ApplicationReadyEvent — Server Accepting Traffic

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.

Initialization Callback Options
@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

Reducing Startup Time

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

Production-Grade Logging Patterns
// 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

application.yml Logging Config
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.

JSON Structured Logging with Logstash Encoder
// 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.

Request Correlation with MDC
@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)
        }
    }
}
Production Logging Anti-Patterns

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.

DevTools Setup
// 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
IntelliJ + DevTools Setup

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.

Actuator Setup and Endpoint Configuration
# 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
Key Actuator Endpoints
# 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

Custom Health Check
@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

Add Build Info to /actuator/info
// 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

Never Expose Actuator Without Security

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.

Actuator Security Configuration
@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

K8s Liveness & Readiness Probes
# 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.
Quick Check
Your Spring Boot app in Kubernetes is processing a long-running database migration at startup. The migration takes 45 seconds. Your readiness probe starts checking after 10 seconds. What happens, and what's the correct fix?
The pod restarts — the liveness probe fails and Kubernetes kills it
Traffic is routed to the pod and requests fail during migration
The pod is healthy but removed from the load balancer until readiness returns UP — increase initialDelaySeconds or implement AvailabilityChangeEvent to control readiness state programmatically
Kubernetes waits indefinitely with no traffic routing
Correct. The readiness probe returning DOWN correctly keeps traffic away from a pod that isn't ready. The liveness probe (separate from readiness) would only trigger a restart if it failed. The fix: increase 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.

CRITICAL hibernate.ddl-auto = create or create-drop in production

Wipes your schema on every startup. Use validate or none. Manage schema with Flyway or Liquibase.

CRITICAL Actuator endpoints exposed without authentication

Exposes heap dumps, config values, shutdown endpoint. Secure with authentication or restrict to internal network.

HIGH Secrets in application.yml committed to version control

Use environment variables or a secrets manager. Never commit passwords, API keys, or tokens.

HIGH Default HikariCP pool size (10) for high-traffic services

Default pool of 10 connections throttles under load. Size appropriately: spring.datasource.hikari.maximum-pool-size.

HIGH No readiness/liveness probes in Kubernetes

K8s routes traffic to pods before they're ready. Use Actuator health endpoints as probe targets.

MEDIUM @SpringBootApplication in a top-level package scanning too broadly

Causes slow startup and unexpected bean discovery. Always put it in a specific sub-package.

MEDIUM DevTools included in production JAR

Causes performance overhead and extra restarts. Use <optional>true</optional> in pom.xml — DevTools auto-disables when running as a JAR.

MEDIUM Ignoring startup warnings in logs

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.

Q: What is the difference between Spring and Spring Boot?
Spring Framework provides the core infrastructure (DI, AOP, MVC, Data). Spring Boot adds opinionated auto-configuration, embedded servers, starter dependencies, and production-ready features on top. Spring Boot doesn't replace Spring — it accelerates Spring setup and reduces boilerplate configuration.
Q: How does Spring Boot auto-configuration work?
Spring Boot reads auto-configuration classes from 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.
Q: What does @SpringBootApplication do?
It's a composed annotation equivalent to @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.
Q: What is the difference between @Value and @ConfigurationProperties?
@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.
Q: Your Spring Boot app works in dev but fails in production with "Could not find bean of type DataSource." What are the likely causes?
Three main causes: (1) The JDBC driver jar is missing from the production classpath — datasource auto-config requires it. (2) 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.
Q: How would you add feature flags to a Spring Boot application using profiles?
Use @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.
Q: How would you reduce the startup time of a large Spring Boot application?
Approaches in order of impact: (1) Narrow component scan base package. (2) Set 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.
Q: How do you manage secrets in a production Spring Boot application?
Never in files committed to VCS. Options by security level: (1) Environment variables injected by the orchestration platform (Docker, K8s). (2) Kubernetes Secrets mounted as environment variables or volume files. (3) HashiCorp Vault with Spring Cloud Vault for dynamic, rotating secrets. (4) AWS Secrets Manager or Azure Key Vault with Spring Cloud AWS/Azure integration. The application config references ${SECRET_NAME} without knowing the source.
Q: What Spring Boot Actuator endpoints would you expose in production, and how would you secure them?
Expose: 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.
🚀

Section 03 Complete

You now understand how Spring Boot actually works — not just how to use it. You can explain auto-configuration, configure environments correctly, secure Actuator, and diagnose production failures.