Spring Boot Basics

How auto-configuration actually works under the hood, not just "it's magic"

← Back to Index

What is Spring Boot, and What Problem Does It Solve?

Spring Boot is not a different framework from Spring — it's an opinionated layer built on top of Spring Framework (covered in Spring Core) that eliminates manual configuration. Before Boot, building a Spring web application meant hand-writing XML or Java configuration to wire a DispatcherServlet, configure a DataSource, set up a TransactionManager, and deploy a WAR to an external Tomcat — hours of setup before writing a single line of business logic.

The problem Boot solves: for the overwhelming majority of applications, that configuration is the same boilerplate every time. Boot inspects what libraries are on your classpath and automatically configures sensible defaults — add spring-boot-starter-web, and Boot infers "this is a web app", wires an embedded Tomcat, registers a DispatcherServlet, and configures Jackson for JSON — all before you write any configuration code. You override only what genuinely differs from the defaults.

Aspect Traditional Spring Spring Boot
Configuration Manual XML or Java @Configuration classes Auto-configuration, override only what differs
Server Deploy WAR to external Tomcat Embedded server, runs as java -jar
Dependencies Manage each version individually Starters + BOM manage compatible versions together
Setup time Hours of wiring before "Hello World" runs Minutes — generate at start.spring.io, run immediately

The Entry Point: @SpringBootApplication

package com.company.myapp;

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

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

// @SpringBootApplication is a meta-annotation — shorthand for three separate ones:
@Configuration            // this class itself can declare @Bean methods
@EnableAutoConfiguration  // triggers the auto-configuration mechanism (see below)
@ComponentScan            // scans this package and sub-packages for @Component/@Service/etc.
# Running it
mvn spring-boot:run                                   # dev mode, via the Maven plugin

mvn clean package
java -jar target/my-app-1.0.0.jar                      # the actual deployable artifact

java -jar my-app.jar --spring.profiles.active=prod     # activate a profile
java -Dserver.port=9090 -jar my-app.jar                 # override a property via system property

How Auto-Configuration Actually Works

This is the part most tutorials wave away as "magic". It isn't — it's a deliberate, inspectable mechanism, and understanding it is what separates someone who uses Spring Boot from someone who can debug it when it does something unexpected.

/*
 *  At startup, @EnableAutoConfiguration triggers this sequence:
 *
 *  1. Spring Boot reads a file bundled inside every starter JAR:
 *     META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
 *     (renamed from the old spring.factories in Boot 2.7+)
 *
 *  2. This file lists candidate @Configuration classes — e.g. spring-boot-
 *     autoconfigure.jar lists DataSourceAutoConfiguration, WebMvcAutoConfiguration,
 *     JacksonAutoConfiguration, and ~200 others.
 *
 *  3. EACH candidate class is conditionally evaluated using @ConditionalOnClass,
 *     @ConditionalOnMissingBean, @ConditionalOnProperty, etc.
 *
 *  4. Only configurations whose conditions are satisfied actually register beans.
 *     The rest are silently skipped — no error, no log by default, just absent.
 */

A real auto-configuration class, simplified

// This is genuinely close to what DataSourceAutoConfiguration looks like internally
@Configuration
@ConditionalOnClass(DataSource.class)              // only activates if a JDBC DataSource class exists on the classpath
@ConditionalOnMissingBean(DataSource.class)        // only if YOU haven't already defined your own DataSource bean
public class DataSourceAutoConfiguration {
    @Bean
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
}
// This is WHY defining your own @Bean DataSource silently disables Boot's
// default — @ConditionalOnMissingBean backs off the moment yours exists.
// No error, no warning — your bean simply wins.
# Exclude specific auto-configurations explicitly
@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    SecurityAutoConfiguration.class
})
public class MyApp { }

# See EXACTLY what was applied and what was skipped, and why —
# the single most useful debugging command for "why isn't my bean here"
java -jar my-app.jar --debug
# Prints a full report: "Positive matches" (applied) and "Negative matches"
# (skipped, with the exact condition that failed) for every candidate
When auto-configuration fights your own @Bean

If you define your own @Bean of a type Boot also tries to auto-configure, @ConditionalOnMissingBean almost always means yours wins silently — Boot's auto-config simply doesn't fire. This is by design, but it's also the source of most "why is my custom configuration being ignored / why is it working differently than I configured" bug reports. The --debug report is the fastest way to confirm whether a given auto-configuration actually ran.

A Minimal REST API

// Model — a record, not a class with manual getters/setters/constructors.
// Immutable, equals/hashCode/toString generated, accessors without "get" prefix.
public record User(Long id, String name, String email) {}

// Service — stateless singleton bean, constructor injection
@Service
public class UserService {
    private final Map<Long, User> users = new ConcurrentHashMap<>();
    private final AtomicLong nextId = new AtomicLong(1);

    public List<User> findAll() { return new ArrayList<>(users.values()); }

    public Optional<User> findById(Long id) { return Optional.ofNullable(users.get(id)); }

    public User create(String name, String email) {
        User user = new User(nextId.getAndIncrement(), name, email);
        users.put(user.id(), user);
        return user;
    }
}

// Controller — thin, delegates everything to the service
@RestController
@RequestMapping("/api/users")
public class UserController {
    private final UserService userService;
    public UserController(UserService userService) { this.userService = userService; }

    @GetMapping
    public List<User> getAll() { return userService.findAll(); }

    @GetMapping("/{id}")
    public ResponseEntity<User> getOne(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User create(@RequestBody CreateUserRequest request) {
        return userService.create(request.name(), request.email());
    }
}

// Separate request DTO from the domain record — never bind @RequestBody
// directly to your entity/domain type in real applications (see Spring REST page)
public record CreateUserRequest(String name, String email) {}

Full HTTP verb coverage, validation, error handling, and DTO patterns are covered in depth in Spring REST — this is the minimal shape to get something running.

DevTools — Faster Local Development

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>  <!-- doesn't propagate transitively to projects that depend on YOUR jar -->
</dependency>

DevTools triggers an automatic restart when classpath files change, disables template/cache defaults that would otherwise hide your edits, and automatically excludes itself from a production build via spring-boot-maven-plugin's repackage step — you don't need to remember to remove it before shipping.

Configuration, Profiles, and Starters — Where to Go Next

Three large topics that deserve their own pages rather than a cramped summary here:

  • Application Propertiesapplication.properties/.yml, @Value vs @ConfigurationProperties, externalised config precedence order.
  • Profiles & Environmentsspring.profiles.active, per-environment property files, @Profile-conditional beans.
  • Spring Boot Starters — what a starter actually is, how to read one, building your own custom starter.

Actuator — Production Observability

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.properties — endpoints are NOT exposed by default except /health
management.endpoints.web.exposure.include=health,info,metrics,env
management.endpoint.health.show-details=when-authorized
EndpointPurpose
/actuator/healthLiveness/readiness — what Kubernetes probes hit
/actuator/metricsMicrometer metrics — JVM, HTTP, custom
/actuator/envResolved environment properties — sensitive, secure it
/actuator/beansFull bean graph — invaluable for debugging DI issues
/actuator/mappingsEvery registered @RequestMapping path
Never expose all Actuator endpoints publicly

/actuator/env, /actuator/heapdump, and /actuator/beans can leak secrets, memory contents, or internal architecture to anyone who can reach them. In production, expose only what's needed (health, info, metrics for monitoring scrapers), put Actuator behind authentication, and ideally bind it to a separate management port (management.server.port) not reachable from the public internet.

Common Pitfalls

Component scan misses classes outside the base package
// ❌ MyAppApplication is in com.company.myapp — @ComponentScan only
// scans THIS package and below. A @Service in com.othercompany.lib is invisible.
package com.company.myapp;
@SpringBootApplication
public class MyAppApplication { }

// ✅ Explicitly widen the scan, or restructure packages so everything
// genuinely sits under the application's root package
@SpringBootApplication(scanBasePackages = {"com.company.myapp", "com.othercompany.lib"})
public class MyAppApplication { }
WAR deployment when you don't need it

Packaging as WAR for an external servlet container is rarely necessary in 2026 — it exists mainly for organisations with established Tomcat/ Jakarta EE infrastructure they're not ready to retire. For new projects, the executable JAR with embedded server is simpler, easier to containerise (the entire deployment unit is one JAR), and is what Spring Boot's own defaults assume.

Interview Questions

🎓 Junior level

Q: What is the difference between Spring and Spring Boot?
Spring Framework is the underlying technology (IoC container, DI, MVC, AOP). Spring Boot is built on top of it and adds auto-configuration, an embedded server, and curated dependency starters — eliminating most of the manual setup Spring traditionally required. Boot doesn't replace Spring; everything you write in a Boot app is still ordinary Spring code.

Q: What three annotations does @SpringBootApplication combine?
@Configuration (this class can declare beans), @EnableAutoConfiguration (triggers Boot's classpath-based auto-configuration), and @ComponentScan (scans this package and below for stereotype-annotated classes).

Q: Why does a Spring Boot app run as an executable JAR instead of being deployed to Tomcat?
Boot embeds the server (Tomcat by default) directly inside the application's own JAR. The application is self-contained and starts with java -jar app.jar — no separate server installation, no WAR deployment step, simpler containerisation.

🔥 Senior level

Q: Explain the mechanism behind auto-configuration — not just what it does, but how.
At startup, @EnableAutoConfiguration reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (the modern replacement for spring.factories, since Boot 2.7) from every starter on the classpath. This lists candidate @Configuration classes. Each candidate is evaluated against conditions — typically @ConditionalOnClass (is the relevant library actually present?) and @ConditionalOnMissingBean (has the developer already defined their own bean of this type?). Only configurations whose conditions pass register beans; the rest are silently skipped. This is why defining your own DataSource bean transparently disables Boot's auto-configured one, with no explicit exclusion needed.

Q: How would you debug a missing or unexpected bean caused by auto-configuration?
Run with --debug (or debug=true in properties) — Boot prints a full "Auto-configuration report" listing every candidate configuration as a Positive match (applied) or Negative match (skipped, with the exact failing condition named). This is the fastest way to confirm whether a specific auto-configuration ran and why it didn't, rather than guessing from documentation. /actuator/beans (Actuator) is the complementary tool — it shows the actual resolved bean graph at runtime.

Q: Why is exposing /actuator/env or /actuator/heapdump publicly a real security risk?
/actuator/env returns all resolved property sources, often including database credentials, API keys, and secrets pulled from environment variables or config servers — verbatim. /actuator/heapdump returns a full JVM heap dump, which can contain in-memory secrets, session tokens, or PII from any object currently alive. The default management.endpoints.web.exposure.include in modern Boot versions is intentionally minimal (just health) precisely because of these risks — widening it to * in production is a common and serious misconfiguration found in security audits.