Skip to content
Home Java Spring Boot Introduction: Why It Exists and How to Build Your First App

Spring Boot Introduction: Why It Exists and How to Build Your First App

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Advanced Java → Topic 21 of 28
Spring Boot explained for intermediate Java developers — what it solves, how auto-configuration works, and how to build a real REST API from scratch.
⚙️ Intermediate — basic Java knowledge assumed
In this tutorial, you'll learn
Spring Boot explained for intermediate Java developers — what it solves, how auto-configuration works, and how to build a real REST API from scratch.
  • Spring Boot is a launcher and opinion layer on top of the Spring Framework — it does not replace Spring's core concepts of dependency injection, Spring MVC, and Spring Data. It removes the setup work of configuring them.
  • Auto-configuration is conditional, not magical — @ConditionalOn guards evaluate your classpath, existing beans, and application properties to decide what to configure. The Conditions Evaluation Report from --debug makes every decision visible and traceable.
  • Your explicit bean definitions always win over auto-configuration — when you define a bean of the same type, @ConditionalOnMissingBean detects it and the auto-configured default steps aside entirely.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • Spring Boot is a launcher and opinion layer on top of Spring — it removes setup work, it does not replace Spring
  • @SpringBootApplication combines @Configuration + @EnableAutoConfiguration + @ComponentScan in one annotation
  • Auto-configuration is conditional — @ConditionalOn* guards only activate when classpath and bean conditions are met
  • Starter dependencies solve version mismatches — one starter pulls in a curated, tested, compatible set of libraries
  • Your explicit bean definitions always win — auto-configuration backs off when you define your own bean of the same type
  • Placing @SpringBootApplication in the wrong package is the most common startup failure — component scanning only finds beans in the same package or sub-packages
🚨 START HERE
Spring Boot Startup Debugging Cheat Sheet
Quick-reference commands for diagnosing Spring Boot startup and production issues. Each entry maps a specific observable symptom to the commands that get you to the answer fastest.
🟡Port already in use — application fails to bind to port 8080 on startup
Immediate ActionIdentify which process holds the port before changing anything — it might be a previous instance of your own application
Commands
lsof -i :8080
kill -9 $(lsof -t -i :8080)
Fix NowKill the conflicting process using the PID from lsof output, or change server.port in application.properties to an available port. On Windows use: netstat -ano | findstr :8080 then taskkill /PID <pid> /F
🟡Bean not found — NoSuchBeanDefinitionException at startup or NoHandlerFoundException for every endpoint
Immediate ActionPrint the auto-configuration conditions report to see exactly why the expected bean was not created
Commands
java -jar myapp.jar --debug 2>&1 | grep -A 3 'CONDITIONS EVALUATION'
java -jar myapp.jar --debug 2>&1 | grep 'did not match'
Fix NowRead the 'did not match' lines — they tell you the exact @Conditional annotation that failed and what it evaluated. Add the missing starter, fix the package structure, or define the bean explicitly based on the reason.
🔴Application runs out of memory — OutOfMemoryError in logs or OOMKill in Kubernetes
Immediate ActionCapture a heap dump before the process terminates — this is the only window into what was in memory
Commands
jcmd $(pgrep -f spring-boot) GC.heap_dump /tmp/heapdump.hprof
kubectl describe pod <pod-name> | grep -A 5 'Last State'
Fix NowOpen the heap dump in Eclipse MAT and check the dominator tree for the largest retained objects. Add -XX:+HeapDumpOnOutOfMemoryError to get automatic dumps on the next occurrence. Increase -Xmx as a temporary measure while investigating root cause.
🟡Database connection refused on startup — ECONNREFUSED to localhost:5432 or data source URL error
Immediate ActionVerify the database process is running and the connection details in application.properties match
Commands
docker ps | grep postgres
pg_isready -h localhost -p 5432 -U forge_admin
Fix NowStart the database container if it is not running. Verify spring.datasource.url, spring.datasource.username, and spring.datasource.password in application.properties. For local development, switch to H2 in-memory database to decouple development from a running database server.
Production IncidentThe Database That Vanished on Every Restart — ddl-auto=create-drop in ProductionA team tested locally with H2 using the default ddl-auto setting, deployed to production with PostgreSQL, and every application restart wiped the entire database schema along with all customer data.
SymptomCustomer data disappeared completely every time the application was redeployed. Support tickets flooded in after each release cycle — users reported empty account dashboards, missing order history, and reset preferences. The database had data before the deployment window began and was completely empty after the application came back online. The operations team initially confirmed the database itself was healthy and accessible.
AssumptionThe team spent a full day investigating the CI/CD pipeline, suspecting the deployment process was pointing to the wrong database instance or that a recent backup restore had been triggered erroneously. They audited connection strings, deployment scripts, and database access logs. Every check came back clean — the right database, the right credentials, the right host. The problem was not in the infrastructure.
Root causespring.jpa.hibernate.ddl-auto was never explicitly set in application.properties. When Spring Boot detects an embedded database like H2, it defaults ddl-auto to create-drop — Hibernate drops all tables when the application shuts down and recreates them fresh on the next startup. This behavior is intentional for local development where the database is throwaway. When the team deployed to production with PostgreSQL, no one noticed that this default had followed them. Hibernate connected to the real production database and dropped every table on each application shutdown, then recreated the empty schema on startup. The data was not migrated somewhere else — it was deleted.
Fixspring.jpa.hibernate.ddl-auto=validate was set explicitly in application-prod.properties — this instructs Hibernate to check that the existing database schema matches the entity definitions without making any modifications. Flyway was adopted for versioned database migrations, with all schema changes defined in numbered SQL files checked into the repository alongside application code. A deployment checklist was added that requires explicit verification of ddl-auto values per environment before any release is approved. Integration tests were added that assert ddl-auto is not set to create or create-drop when the active Spring profile is prod.
Key Lesson
Never rely on default ddl-auto settings — explicitly set spring.jpa.hibernate.ddl-auto for every environment in its own application-{profile}.properties filecreate-drop is only safe for in-memory development databases where data is intentionally ephemeral — it destroys all table data on every application shutdown without warningUse validate or none in production and manage all schema changes through versioned migrations with Flyway or Liquibase — schema changes become code changes with history, review, and rollbackTest against the same database engine in staging as in production — H2 compatibility mode masks configuration behaviors that only surface with PostgreSQL or MySQL under real conditionsAdd a startup assertion or integration test that fails if ddl-auto is create or create-drop outside of a development or test profile — catches the configuration error before it reaches production
Production Debug GuideWhen your Spring Boot application fails to start or behaves unexpectedly at startup, here is the diagnostic sequence that gets to root cause fastest.
Every endpoint returns 404 with NoHandlerFoundException and no controller error in the logsCheck the package structure of your @SpringBootApplication class relative to your controllers. If the main class is in io.thecodeforge.app but controllers are in com.other.controllers, @ComponentScan will not find them — those packages are not sub-packages of the main class package. Move the main class to the common root package, or add @ComponentScan(basePackages = {"io.thecodeforge", "com.other"}) explicitly. Verify the fix by checking the startup log for 'Mapped request' lines that list your endpoints.
Expected bean is missing from the ApplicationContext — NoSuchBeanDefinitionException at injection pointsRun the application with --debug flag and read the CONDITIONS EVALUATION REPORT in the startup output. Find the auto-configuration class that should create the missing bean and look for 'did not match' alongside it. Common causes are a missing classpath dependency (@ConditionalOnClass failed), an existing user-defined bean of the same type (@ConditionalOnMissingBean triggered), or a required property not being set (@ConditionalOnProperty failed). The report tells you the exact condition that failed in plain text.
Application crashes on startup with PortAlreadyInUseException — address already in use on port 8080Find which process is holding the port: lsof -i :8080 on Mac/Linux or netstat -ano | findstr :8080 on Windows. If it is another instance of your application that did not terminate cleanly (common in development), kill it with kill -9 <PID>. If it is a different service, change the port with server.port=8081 in application.properties. In Docker environments, check for port mapping conflicts between containers or between the host and the container.
NoSuchBeanDefinitionException at startup — a dependency cannot be auto-wiredCheck three things in sequence: (1) does the missing bean's class require a specific starter that is not in pom.xml, (2) is a @ConditionalOn guard preventing auto-configuration from creating it — check the Conditions Report under Negative matches, (3) is the bean's class in a package that @ComponentScan reaches — the class must be in the same package as @SpringBootApplication or a sub-package of it.
Application starts but all JSON responses have missing fields or the wrong formatCheck if you have a custom ObjectMapper @Bean defined anywhere in the application. If you do, JacksonAutoConfiguration backs off and your ObjectMapper is used instead — without the default Spring Boot configuration applied to it. Run with --debug and look for JacksonAutoConfiguration in the Negative matches section. If it shows 'did not match' with reason 'existing bean of type ObjectMapper', your custom ObjectMapper is taking over. Either remove it, or configure it explicitly with the Spring Boot defaults as a starting point.
Application starts slowly — over 30 seconds for a simple REST API with no complex initializationRun with --debug and count the Negative matches in the Conditions Evaluation Report. Each auto-configuration class on the list is evaluated at startup even if it is rejected. Remove starters for technologies you are not using — spring-boot-starter-data-mongodb, spring-boot-starter-amqp, spring-boot-starter-data-redis — each pulls in auto-configuration classes that must be evaluated even if all conditions fail. As a temporary measure while identifying the real culprits, try spring.main.lazy-initialization=true to defer bean creation until first use.

Every Java developer hits the same wall. You have a clear idea for a web service, but before writing a single line of business logic you are buried in XML configuration, manually wiring beans, registering a servlet container, and fighting dependency version mismatches between libraries that were never designed to be used together. By the time the environment actually runs, you have forgotten what you were building in the first place.

Spring Boot was created specifically to eliminate that wall. It is why almost every new Java backend project in the industry starts with it today, and why it has become the default answer to 'how do we build a new microservice?' regardless of team size or company.

The core problem Spring Boot solves is bootstrapping friction. The original Spring Framework is genuinely powerful — the dependency injection model, Spring MVC, Spring Data — but famously verbose to configure. A simple REST endpoint in raw Spring MVC could require a web.xml descriptor, a DispatcherServlet configuration, a Spring context configuration class, and a dozen explicit bean definitions before you could write your first business method. Spring Boot replaces all of that with intelligent defaults and conditional auto-configuration: it looks at what is on your classpath and wires things up for you automatically. You bring the feature; Spring Boot brings the scaffolding.

I have set up both from scratch. Configuring raw Spring MVC for a hello-world REST endpoint in 2015 took me the better part of a morning. The equivalent Spring Boot application took under four minutes. That gap has only grown as the ecosystem matured.

By the end of this guide you will understand exactly what Spring Boot is and what it is not, why auto-configuration is the engine that makes everything else work, how to build and run a real layered REST API, and where the specific traps are that catch even experienced developers off guard — including the one that wiped a production database.

What Spring Boot Actually Is — and What It Isn't

Spring Boot is not a replacement for the Spring Framework. This trips up a lot of developers, especially those coming from other ecosystems who assume 'Spring Boot' and 'Spring' are the same thing. They are not. Spring Boot is a launcher and opinion layer built on top of the Spring Framework. It uses the same Spring core underneath — the same dependency injection container, Spring MVC for HTTP handling, Spring Data for database access — but it makes opinionated decisions about how those pieces fit together so you do not have to negotiate that assembly process yourself.

Three pillars hold up everything Spring Boot does.

The first is Auto-Configuration. When your application starts, Spring Boot scans what is on your classpath and automatically creates beans you would otherwise define manually. If it sees spring-boot-starter-web on the classpath, it auto-configures an embedded Tomcat server, registers a DispatcherServlet, configures Jackson for JSON serialization, and sets up error handling. No XML. No explicit bean declarations for any of this. The configuration infers itself from what you brought to the project.

The second is Starter Dependencies. Instead of hunting for compatible Maven or Gradle versions of a dozen different Spring libraries — a process that reliably produces version conflicts — you add one starter and it pulls in everything that works together, versioned correctly. spring-boot-starter-data-jpa gives you Hibernate, Spring Data JPA, a connection pool, and JDBC in one dependency declaration with tested version compatibility.

The third is the Embedded Server. Your application ships as a single executable JAR with Tomcat (or Jetty or Undertow) embedded inside it. No deploying WAR files to an external application server. No managing server versions. No checking whether the target Tomcat version supports your servlet API version. You run java -jar myapp.jar and the server starts.

The most important mental model: Spring Boot does not add features to Spring. It removes the work of configuring them. When you write a controller, a service, a repository — that is Spring. When those components get wired together and an HTTP server starts without you touching a configuration file, that is Spring Boot.

io/thecodeforge/bookstore/BookstoreApplication.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041
package io.thecodeforge.bookstore;

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

/**
 * io.thecodeforge: The entry point for the Bookstore API.
 *
 * @SpringBootApplication is a composed annotation — three annotations in one:
 *
 *   @SpringBootConfiguration   (specialization of @Configuration)
 *   -> Marks this class as a source of @Bean definitions
 *   -> This class can declare beans directly if needed
 *
 *   @EnableAutoConfiguration
 *   -> Triggers auto-configuration by reading AutoConfiguration.imports
 *      from every JAR on the classpath
 *   -> Each listed config class is evaluated against @ConditionalOn guards
 *   -> Only classes where all conditions pass are activated
 *
 *   @ComponentScan
 *   -> Scans this package (io.thecodeforge.bookstore) and ALL sub-packages
 *   -> Finds @Component, @Service, @Repository, @Controller, @RestController
 *   -> Registers discovered classes as beans in the ApplicationContext
 *
 * Package placement rule: this class MUST be in the root package.
 * Controllers in io.thecodeforge.bookstore.controller -> found.
 * Controllers in com.other.controller -> NOT found. 404 on every endpoint.
 */
@SpringBootApplication
public class BookstoreApplication {

    public static void main(String[] args) {
        // SpringApplication.run:
        // 1. Creates the ApplicationContext (the Spring IoC container)
        // 2. Registers all auto-configured beans
        // 3. Scans for @Component classes and registers them
        // 4. Starts the embedded Tomcat server on port 8080
        SpringApplication.run(BookstoreApplication.class, args);
    }
}
▶ Output
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
:: Spring Boot :: (v3.3.0)

INFO --- [main] i.t.b.BookstoreApplication : Starting BookstoreApplication using Java 21
INFO --- [main] i.t.b.BookstoreApplication : No active profile set, falling back to default profiles: default
INFO --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
INFO --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
INFO --- [main] i.t.b.BookstoreApplication : Started BookstoreApplication in 2.847 seconds (process running for 3.1)
Mental Model
Spring Boot Is a Scaffold, Not a Replacement
Spring Boot removes the work of configuring Spring — it does not add new features to Spring itself. When you understand Spring, you understand why Spring Boot makes the choices it does.
  • Auto-configuration scans your classpath and creates beans you would otherwise define manually — it fills gaps and backs off when you provide your own definitions
  • Starters are curated, tested dependency groups — one starter replaces manually hunting for 12 compatible library versions and eliminates version conflict errors
  • The embedded server means your application is a self-contained artifact — one JAR, one command, same behavior on every machine that has Java installed
  • @SpringBootApplication is shorthand for three annotations — break them apart with @ComponentScan(basePackages=...) when your code spans multiple package trees
  • Spring Boot is opinionated but overridable — define your own bean and the auto-configured default backs off automatically, which is the mechanism, not a side effect
📊 Production Insight
A team placed their @SpringBootApplication class in io.thecodeforge.app while their controllers lived in com.company.controllers — a different package tree entirely, not a sub-package. @ComponentScan found nothing in com.company because it was never instructed to look there. Every HTTP endpoint returned 404 with no error anywhere in the logs — the controllers were not broken, they simply did not exist from Spring's perspective. The team spent six hours debugging routing configuration, filter chains, and security settings before someone compared the main class package to the controller package. The fix was one line: moving the main class to the common root package. The lesson that stuck was checking package structure before anything else when endpoints return 404 without explanation.
🎯 Key Takeaway
Spring Boot is a launcher and opinion layer on top of Spring — it removes setup work by making conditional wiring decisions based on your classpath, it does not replace Spring.
The three pillars are auto-configuration, starter dependencies, and the embedded server — each eliminates a distinct category of manual setup work.
Package placement of @SpringBootApplication is the most common source of silent 404 failures — the main class must be at the root package that is the parent of all other packages in your application.
Component Scanning and Package Structure Decisions
IfAll application code lives under one package tree — for example, io.thecodeforge.bookstore.*
UsePlace @SpringBootApplication at the root package (io.thecodeforge.bookstore) — component scan finds all sub-packages automatically
IfCode spans multiple package trees — for example, io.thecodeforge and com.company.shared
UseUse @ComponentScan(basePackages = {"io.thecodeforge", "com.company.shared"}) to explicitly declare all roots — do not rely on default scanning to find packages outside the main class package
IfNeed to exclude specific packages from scanning — test fixtures, generated code, or legacy components
UseUse @ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "io\\.thecodeforge\\.generated\\..*")) to exclude by pattern
IfMulti-module Maven or Gradle project with separate modules for controller, service, and repository layers
UseEnsure the main application class module includes all other modules as dependencies and that sub-module packages are children of the root package declared in the main class
IfLibrary JAR provides beans that should be auto-configured in consuming applications
UseRegister the library's configuration class in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports — component scan in the consuming application will not find it, only auto-configuration discovery will

Auto-Configuration: The Engine Under the Hood

Auto-configuration is the most powerful feature in Spring Boot and the most misunderstood concept when developers first encounter it. Candidates in interviews say 'Spring Boot configures itself automatically' as though that is a complete explanation. It is not. The mechanism behind that statement is what determines whether you can actually debug Spring Boot when it misbehaves — and it will misbehave.

When your application starts, Spring Boot invokes AutoConfigurationImportSelector, which reads a file called META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports from every JAR on your classpath. In Spring Boot 2.x this was META-INF/spring.factories — same idea, different file. The spring-boot-autoconfigure JAR alone lists over 150 configuration classes in this file. Every starter you add to pom.xml can contribute more.

Each listed class is annotated with @ConditionalOn guards that function as evaluation criteria. DataSourceAutoConfiguration has @ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class}) and @ConditionalOnMissingBean(DataSource.class). Both conditions must pass before the class is activated and its @Bean methods are executed. If there is no JDBC driver class on the classpath, the first condition fails and the entire DataSourceAutoConfiguration class is skipped. If you have already defined your own DataSource bean, the second condition fails and it is skipped again. Either way, nothing is created.

This is the model that makes 'opinionated but overridable' work in practice. You do not fight auto-configuration to override it. You simply define your own bean and the auto-configured one backs off. Want a custom ObjectMapper with specific date formatting? Define a @Bean that returns an ObjectMapper and JacksonAutoConfiguration detects it via @ConditionalOnMissingBean and steps aside entirely.

The practical implication for debugging: when something is not working the way you expect, the question is always 'is the relevant auto-configuration firing or not, and why?' The --debug flag answers this definitively. Run java -jar myapp.jar --debug and look for the CONDITIONS EVALUATION REPORT section in the output. Positive matches show what fired. Negative matches show what was skipped and the exact condition that caused the skip. There is no guessing required — the report tells you precisely what happened and why.

io/thecodeforge/bookstore/controller/BookController.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
package io.thecodeforge.bookstore.controller;

import io.thecodeforge.bookstore.model.Book;
import io.thecodeforge.bookstore.service.BookService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * io.thecodeforge: REST controller for book operations.
 *
 * Auto-configuration that made this controller work without any manual setup:
 *   - @RestController is found by @ComponentScan (part of @SpringBootApplication)
 *   - Spring MVC DispatcherServlet was auto-configured by WebMvcAutoConfiguration
 *     because spring-boot-starter-web is on the classpath
 *   - Jackson ObjectMapper was auto-configured by JacksonAutoConfiguration
 *     — Book objects are serialized to JSON automatically
 *   - Embedded Tomcat was started by EmbeddedWebServerFactoryCustomizerAutoConfiguration
 *
 * None of these required manual bean definitions. The classpath told Spring Boot
 * what to configure and the @ConditionalOn guards made it happen.
 */
@RestController
@RequestMapping("/api/books")
public class BookController {

    // Constructor injection: the correct pattern in production Spring Boot code.
    // Dependencies are final — immutable after construction.
    // Class is testable without a Spring context — new BookController(mockService).
    // Missing dependencies cause a compile-time error, not a runtime NullPointerException.
    private final BookService bookService;

    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping
    public ResponseEntity<List<Book>> getAllBooks() {
        return ResponseEntity.ok(bookService.findAllBooks());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Book createBook(@RequestBody Book newBook) {
        return bookService.saveBook(newBook);
    }

    @GetMapping("/{bookId}")
    public ResponseEntity<Book> getBookById(@PathVariable Long bookId) {
        // Optional.map avoids a null check — if the book exists, return 200 with it.
        // If it does not exist, return 404 with an empty body.
        // orElse(ResponseEntity.notFound().build()) handles the absent case cleanly.
        return bookService.findBookById(bookId)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{bookId}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteBook(@PathVariable Long bookId) {
        bookService.deleteBook(bookId);
    }
}
▶ Output
// POST /api/books with body {"title": "Clean Code", "author": "Robert Martin", "price": 29.99}
HTTP 201 Created
{
"id": 1,
"title": "Clean Code",
"author": "Robert Martin",
"price": 29.99
}

// GET /api/books/1
HTTP 200 OK
{
"id": 1,
"title": "Clean Code",
"author": "Robert Martin",
"price": 29.99
}

// GET /api/books/9999
HTTP 404 Not Found
(empty body — ResponseEntity.notFound().build())

// DELETE /api/books/1
HTTP 204 No Content
(empty body — @ResponseStatus(HttpStatus.NO_CONTENT))
💡The Conditions Evaluation Report Is Your Primary Debugging Tool
Run java -jar myapp.jar --debug and look for the CONDITIONS EVALUATION REPORT in the startup output. It is divided into three sections: Positive matches (auto-configurations that fired and created beans), Negative matches (auto-configurations that were skipped and the exact condition that caused the skip), and Unconditional classes (always activated). Every 'Negative matches' entry shows the class name, the @Conditional annotation that failed, and what it evaluated. If DataSourceAutoConfiguration is in Negative matches with 'did not find required class com.zaxxer.hikari.HikariDataSource', you know exactly what dependency is missing. If it says 'existing bean of type javax.sql.DataSource found', you know your own bean is in control. I reach for this report before any other debugging step when a bean is missing or a feature is not activating as expected. It removes all guesswork.
📊 Production Insight
A team added spring-boot-starter-data-jpa to a service that was not yet connected to a database. The starter's transitive dependencies included HikariCP. DataSourceAutoConfiguration saw HikariCP's class on the classpath, found no user-defined DataSource bean, and attempted to create a connection pool using the default localhost:5432 target. The application crashed at startup with a connection refused error to a database that did not exist in their environment. The team spent three hours investigating network configuration and firewall rules. Running --debug would have shown DataSourceAutoConfiguration in Positive matches within seconds of the crash, making the cause immediately obvious. After this incident, the team added 'run once with --debug and verify Positive matches' to their new-service checklist.
🎯 Key Takeaway
Auto-configuration is conditional assembly driven by @ConditionalOn guards — it is not magic, it is a traceable decision process that evaluates your classpath, existing beans, and application properties.
Your explicit bean definitions always win — auto-configuration never overrides a bean you have defined, it detects it via @ConditionalOnMissingBean and backs off.
The CONDITIONS EVALUATION REPORT from --debug is the definitive answer to 'why is this auto-configuration not firing' — check it before investigating anything else.
Debugging Auto-Configuration Issues
IfExpected bean is missing from the context at startup — injection fails with NoSuchBeanDefinitionException
UseRun with --debug and find the relevant auto-configuration class in Negative matches — the report shows the exact @Conditional condition that failed and what it evaluated
IfAuto-configuration fires but uses wrong settings — wrong database URL, wrong connection pool size
UseOverride the specific properties in application.properties (spring.datasource.url, spring.datasource.hikari.maximum-pool-size) — redefining the entire bean is rarely necessary
IfAuto-configuration conflicts with your custom configuration — NoUniqueBeanDefinitionException
UseAdd @Primary to your custom bean for unambiguous resolution, or exclude the conflicting auto-configuration via spring.autoconfigure.exclude in application.properties
IfApplication starts slower than expected — many auto-configurations evaluated unnecessarily
UseRemove unused starters from pom.xml — each one pulls in auto-configuration classes that are evaluated at every startup even when all conditions fail
IfCustom auto-configuration class in a library JAR is never discovered
UseRegister the class in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports — component scanning does not discover auto-configuration classes, only the imports file does

Building the Complete Bookstore: Model, Service, Repository, and Configuration

A controller without the layers beneath it is not a real application — it is a sketch. The production pattern in Spring Boot is a clear separation of concerns: the controller handles HTTP concerns, the service contains business logic, the repository handles data access, and the model defines the domain shape. Each layer has a single responsibility and can be tested independently.

This separation matters beyond code organization. When a business rule changes, you modify the service without touching the controller. When you switch from H2 to PostgreSQL, you change application.properties without touching any Java class. When you write a unit test for the service's business logic, you mock the repository and run the test without starting a server or a database.

The Book model below uses @Entity and @Id — Spring Data JPA annotations that map the class to a database table. Spring Data JPA is itself auto-configured when spring-boot-starter-data-jpa is on the classpath and a DataSource bean exists. The pattern repeats across every Spring Boot feature: add a starter, write your domain code, and Spring Boot handles the infrastructure wiring between them.

The application.properties file is where configuration lives outside the code. Port numbers, database URLs, feature flags, connection pool sizes — all externalized here. Spring Boot supports environment-specific property files through profiles: application-dev.properties activates when SPRING_PROFILES_ACTIVE=dev, application-prod.properties activates when SPRING_PROFILES_ACTIVE=prod. The same JAR, the same compiled code, different runtime behavior driven by which profile is active. This is how you have H2 in development and PostgreSQL in production without conditional logic anywhere in your Java code.

io/thecodeforge/bookstore/BookstoreComponents.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
// ── Model ────────────────────────────────────────────────────────────────────
package io.thecodeforge.bookstore.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;

/**
 * io.thecodeforge: JPA entity — maps to the 'books' table.
 *
 * @Entity tells Hibernate this class represents a database table.
 * @Table(name = "books") overrides the default table name (would be 'book').
 * @Id marks the primary key field.
 * @GeneratedValue delegates key generation to the database.
 */
@Entity
@Table(name = "books")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Title is required")
    @Column(nullable = false)
    private String title;

    @NotBlank(message = "Author is required")
    private String author;

    @Positive(message = "Price must be a positive number")
    private double price;

    // JPA requires a no-argument constructor — Hibernate uses it when
    // reconstructing objects from database rows
    public Book() {}

    public Book(String title, String author, double price) {
        this.title = title;
        this.author = author;
        this.price = price;
    }

    public Long getId() { return id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
}

// ── Repository ───────────────────────────────────────────────────────────────
package io.thecodeforge.bookstore.repository;

import io.thecodeforge.bookstore.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * JpaRepository<Book, Long> provides:
 *   findAll(), findById(), save(), delete(), count() — all auto-implemented.
 * Spring Data JPA generates the implementation at startup — no SQL, no boilerplate.
 * Add custom query methods by naming convention: findByAuthor(String author)
 * or by @Query annotation for complex queries.
 */
public interface BookRepository extends JpaRepository<Book, Long> {
    // Custom finder — Spring Data JPA generates: SELECT * FROM books WHERE author = ?
    java.util.List<Book> findByAuthor(String author);
}

// ── Service ──────────────────────────────────────────────────────────────────
package io.thecodeforge.bookstore.service;

import io.thecodeforge.bookstore.model.Book;
import io.thecodeforge.bookstore.repository.BookRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;

@Service
public class BookService {

    private final BookRepository bookRepository;

    // Constructor injection — final field, explicitly documented dependency
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    // readOnly = true: signals Hibernate to skip dirty checking — performance optimization
    // for queries that do not modify data
    @Transactional(readOnly = true)
    public List<Book> findAllBooks() {
        return bookRepository.findAll();
    }

    @Transactional(readOnly = true)
    public Optional<Book> findBookById(Long id) {
        return bookRepository.findById(id);
    }

    @Transactional
    public Book saveBook(Book book) {
        return bookRepository.save(book);
    }

    @Transactional
    public void deleteBook(Long id) {
        bookRepository.deleteById(id);
    }
}
▶ Output
// Application startup — Hibernate DDL output with ddl-auto=update:
Hibernate: create table books (
id bigint generated by default as identity,
author varchar(255) not null,
price float(53),
title varchar(255) not null,
primary key (id)
)

// POST /api/books {"title": "Effective Java", "author": "Joshua Bloch", "price": 45.00}
Hibernate: insert into books (author, price, title) values (?, ?, ?)
HTTP 201 Created — {"id": 1, "title": "Effective Java", "author": "Joshua Bloch", "price": 45.00}

// GET /api/books?author=Joshua%20Bloch (using findByAuthor custom finder)
Hibernate: select b.id, b.author, b.price, b.title from books b where b.author=?
HTTP 200 OK — [{"id": 1, "title": "Effective Java", "author": "Joshua Bloch", "price": 45.00}]
⚠ ddl-auto Is the Most Dangerous Default in Spring Boot — Set It Explicitly for Every Environment
spring.jpa.hibernate.ddl-auto controls what Hibernate does to your database schema at startup and shutdown. The defaults depend on which database you are using, which is part of why this setting causes so much damage when teams do not explicitly set it. With an embedded database like H2, the default is create-drop — Hibernate creates the schema at startup and drops everything at shutdown. This is intentional for development where the database is throwaway. With an external database like PostgreSQL, the default is none — Hibernate does not touch the schema. The production incident described in this guide happened precisely because the team never explicitly set this value. In development with H2, create-drop worked fine. In production with PostgreSQL, the same setting that was implicitly applied from the H2 default carried over and destroyed the schema on every restart. Production rule: ddl-auto=validate checks that the existing schema matches your entities without modifying anything. Schema changes go through Flyway or Liquibase — versioned SQL migration files checked into the repository. Schema migrations are code changes with history, review, and rollback capability.
📊 Production Insight
The ddl-auto incident described in this guide was not an isolated event — I have seen variations of it at three different companies, always following the same pattern. Local development worked for weeks with H2 and create-drop. The team moved to a shared PostgreSQL instance in staging, explicitly set ddl-auto=create to have Hibernate generate the schema for them (convenient, no SQL required). When they promoted the same configuration to production with real customer data, the next deployment dropped and recreated every table. The data was gone. The pattern that prevents this is treating ddl-auto as a deployment configuration value that must be explicitly set in every environment-specific properties file and verified in the deployment checklist — not something inherited from defaults or carried over from a previous environment.
🎯 Key Takeaway
Separation of concerns — controller handles HTTP, service contains business logic, repository handles data access — makes each layer independently testable and maintainable as the application grows.
Never rely on default ddl-auto settings — explicitly set spring.jpa.hibernate.ddl-auto in every environment-specific properties file, use validate or none in production, and manage schema with Flyway or Liquibase.
application.properties externalizes everything that varies between environments — database URL, port, feature flags, connection pool sizes — the same JAR runs differently in each environment without recompilation.
Database Configuration Decisions — Environment by Environment
IfLocal development environment with throwaway test data — no persistent data needed
UseUse H2 in-memory with ddl-auto=create-drop — schema is fresh on every restart, no cleanup needed, fast startup
IfShared development or staging environment with persistent data that should survive restarts
UseUse PostgreSQL or MySQL with ddl-auto=validate and Flyway managing schema — data persists, schema changes are explicit and reviewable
IfProduction environment with customer data
UseUse ddl-auto=validate with Flyway or Liquibase — schema managed in versioned SQL migrations, never modified by Hibernate at runtime
IfNeed to switch database engines across environments without code changes
UseUse Spring profiles with application-dev.properties and application-prod.properties — same compiled code, different runtime configuration controlled by SPRING_PROFILES_ACTIVE
IfNeed to initialize test data on application startup for development or integration testing
UseUse src/main/resources/data.sql with ddl-auto=create — Spring Boot loads data.sql after Hibernate creates the schema, populating tables with test data automatically

The Complete pom.xml: What Goes In and Why

Understanding the pom.xml structure is where the starter model becomes concrete. Most tutorials show you to copy the dependencies section without explaining what each entry does and why it is structured the way it is. That gap creates real problems when something goes wrong — you cannot reason about dependency conflicts or missing features if you do not know what each starter contributes.

The spring-boot-starter-parent entry is doing more work than it appears. It imports the spring-boot-dependencies BOM, which defines the exact version of every library that Spring Boot works with. Every starter you add inherits version management from this BOM — you write the dependency without a version number and the BOM supplies the correct one. This is why you almost never specify versions inside a starter-based project. The BOM guarantees that all 150+ libraries in the ecosystem work together at the versions Spring Boot tested.

The starter pattern consolidates what would otherwise be 8 to 12 individual dependency declarations into one. spring-boot-starter-web alone pulls in spring-web, spring-webmvc, spring-boot-starter, spring-boot-starter-tomcat, spring-boot-starter-json, jackson-databind, and their transitive dependencies — all at compatible versions. Without the starter, you would specify each separately and spend time verifying that your Jackson version works with your Spring MVC version works with your Tomcat version.

pom.xml · XML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!--
      spring-boot-starter-parent provides:
        1. spring-boot-dependencies BOM — version management for 150+ libraries
        2. Default Maven plugin configurations (compiler at Java 17+, test runner)
        3. Resource filtering for application.properties placeholder resolution
      Without this parent, you would need to specify library versions individually
      and verify their compatibility manually.
    -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath/>
    </parent>

    <groupId>io.thecodeforge</groupId>
    <artifactId>bookstore-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>bookstore-api</name>
    <description>Bookstore REST API — io.thecodeforge demonstration project</description>

    <properties>
        <!-- Java 21 LTS — the current long-term support version as of 2026 -->
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <!--
          spring-boot-starter-web pulls in:
            spring-web, spring-webmvc, embedded Tomcat, Jackson JSON serialization.
          Without this: no HTTP server, no @RestController processing, no JSON output.
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--
          spring-boot-starter-data-jpa pulls in:
            Hibernate, Spring Data JPA, JDBC, HikariCP connection pool.
          Without this: no @Entity processing, no JpaRepository, no @Transactional.
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!--
          spring-boot-starter-validation pulls in:
            Hibernate Validator (JSR-303 implementation).
          Without this: @NotBlank, @Email, @Size annotations on DTOs are ignored.
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!--
          spring-boot-starter-actuator adds:
            /actuator/health, /actuator/metrics, /actuator/info endpoints.
          Used for Kubernetes liveness/readiness probes and monitoring integration.
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!--
          H2 in-memory database — runtime scope means it is available at runtime
          but not included in the final production JAR.
          For development and testing only — replace with PostgreSQL driver in production.
        -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--
          spring-boot-starter-test pulls in:
            JUnit 5, Mockito, AssertJ, Spring Test, MockMvc, Testcontainers support.
          test scope — never included in the production JAR.
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!--
              spring-boot-maven-plugin creates the executable Fat JAR.
              Without this plugin, mvn package produces a standard JAR that cannot
              run standalone — it lacks the Spring Boot launcher and embedded server.
              repackage goal wraps the standard JAR inside a Fat JAR with BOOT-INF layout.
            -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
▶ Output
// mvn clean package produces:
// target/bookstore-api-0.0.1-SNAPSHOT.jar (Fat JAR — executable, ~45MB)
// target/bookstore-api-0.0.1-SNAPSHOT.jar.original (thin JAR — not executable)
//
// Inspect the Fat JAR layout:
// jar tf target/bookstore-api-0.0.1-SNAPSHOT.jar | head -20
// META-INF/MANIFEST.MF
// META-INF/
// BOOT-INF/classes/io/thecodeforge/bookstore/BookstoreApplication.class
// BOOT-INF/classes/io/thecodeforge/bookstore/model/Book.class
// BOOT-INF/lib/spring-boot-3.3.0.jar
// BOOT-INF/lib/spring-boot-autoconfigure-3.3.0.jar
// BOOT-INF/lib/spring-web-6.1.0.jar
// BOOT-INF/lib/tomcat-embed-core-10.1.0.jar
// ...(all dependency JARs nested inside)
//
// Run the Fat JAR:
// java -jar target/bookstore-api-0.0.1-SNAPSHOT.jar
// java -jar target/bookstore-api-0.0.1-SNAPSHOT.jar --debug (with Conditions Report)
// java -jar target/bookstore-api-0.0.1-SNAPSHOT.jar --server.port=9090 (override port)
💡Do Not Override Individual Dependency Versions Unless You Have a Specific Reason
The spring-boot-dependencies BOM manages versions for every library in the Spring Boot ecosystem based on testing combinations that are known to work together. When you override a single version — specifying <version>2.15.4</version> on Jackson inside a spring-boot-starter project — you step outside the tested compatibility matrix. The consequences are usually subtle: a method that exists in version 2.15.4 but was removed in 2.16.0, a serialization behavior that changed between minor versions, or a class that is present but has a different signature than what another library expects. These surface as ClassNotFoundException or NoSuchMethodError at runtime, not at compile time, and they are difficult to trace back to the version override. Resist the urge. If you must override for a specific security patch or a required feature, document the exact reason in the pom.xml comment and test thoroughly with the overridden version.
📊 Production Insight
A team overrode the Hibernate version inside spring-boot-starter-data-jpa to get access to a new query feature. The override was undocumented — it was added in a hurry during a feature sprint and forgotten. Six months later, a Spring Boot upgrade changed the expected Hibernate version and the two versions had incompatible SessionFactory APIs. The application failed to start with a cryptic NoSuchMethodError that pointed nowhere obviously useful. The team spent a day investigating before someone ran mvn dependency:tree and noticed the version discrepancy. Adding a comment next to the version override — 'Required for feature X, remove when Spring Boot 3.4+ is adopted' — would have prevented the entire investigation.
🎯 Key Takeaway
spring-boot-starter-parent provides BOM-based version management for 150+ libraries — adding a starter without specifying a version is intentional, not an oversight.
Each starter is a curated dependency group that replaces 8 to 12 individual declarations with tested, compatible versions — this is the practical solution to dependency hell.
Do not override individual library versions inside a BOM-managed project without documentation and thorough testing — incompatibilities surface at runtime, not at compile time.
Choosing Starters and Managing Dependencies
IfBuilding a REST API that returns JSON responses
UseAdd spring-boot-starter-web — provides Spring MVC, embedded Tomcat, and Jackson JSON serialization
IfPersisting data to a relational database with JPA entities
UseAdd spring-boot-starter-data-jpa and a database driver (h2 for development, postgresql or mysql-connector-j for production)
IfNeed DTO validation with @NotBlank, @Email, @Size constraints
UseAdd spring-boot-starter-validation — without it, JSR-303 annotations on DTO classes are ignored entirely
IfDeploying to Kubernetes and need health probes or exposing metrics
UseAdd spring-boot-starter-actuator — provides /actuator/health/readiness, /actuator/health/liveness, and /actuator/metrics without any additional configuration
IfNeed to override a specific library version for a security patch
UseUse Maven properties defined in the parent BOM where possible (e.g., <jackson.version>2.17.0</jackson.version>) rather than hardcoding versions in individual dependency declarations — document the reason in a comment
🗂 Traditional Spring MVC vs. Spring Boot
Spring Boot eliminates configuration overhead by replacing manual XML wiring with convention-based auto-configuration. The trade-off is less explicit visibility into defaults in exchange for dramatically faster time from idea to running application.
Feature / AspectTraditional Spring MVCSpring Boot
Setup time for a working REST endpoint30 to 60 minutes — web.xml, dispatcher config, context config, explicit bean declarations all required before the first line of business logicUnder 5 minutes — starter dependency, @SpringBootApplication, @RestController, done
Server deployment modelCompile to WAR, deploy to external Tomcat or JBoss, manage server version separately from application versionCompile to Fat JAR, run with java -jar — server version is part of the application, consistent across every environment
Dependency managementManually specify compatible versions for Spring core, Spring MVC, Jackson, validation API, Hibernate — version conflicts are commonDeclare starters without versions — spring-boot-dependencies BOM manages compatible versions for the entire ecosystem
Configuration styleMostly explicit XML or @Configuration Java classes — every bean, every mapping, every serializer declared manuallyConvention-based with application.properties overrides — declare nothing for the common case, override specific settings when needed
Auto-configurationNone — every bean must be declared explicitly, nothing is inferred from the classpathConditional auto-configuration based on classpath, existing beans, and properties — the Conditions Evaluation Report makes every decision transparent
Production readinessManual setup required for health endpoints, metrics, and monitoring integration — each team implements differentlyspring-boot-starter-actuator adds /actuator/health, /actuator/metrics, and /actuator/loggers instantly — Kubernetes-compatible probes out of the box
Learning curveSteep — many moving parts that must be understood and wired together before anything worksGentle entry point, same Spring depth available — you can be productive quickly and go deep when you need to
Best forMaintaining existing Spring applications already running in production on external servers with established deployment pipelinesAny new Java backend project regardless of size — the defaults are production-grade and everything is overridable when your requirements differ

🎯 Key Takeaways

  • Spring Boot is a launcher and opinion layer on top of the Spring Framework — it does not replace Spring's core concepts of dependency injection, Spring MVC, and Spring Data. It removes the setup work of configuring them.
  • Auto-configuration is conditional, not magical — @ConditionalOn guards evaluate your classpath, existing beans, and application properties to decide what to configure. The Conditions Evaluation Report from --debug makes every decision visible and traceable.
  • Your explicit bean definitions always win over auto-configuration — when you define a bean of the same type, @ConditionalOnMissingBean detects it and the auto-configured default steps aside entirely.
  • Constructor injection is the correct way to wire dependencies — it makes dependencies explicit, enables final field immutability, and allows unit tests to use the class without starting a Spring context.
  • Package placement of @SpringBootApplication is the most common source of silent 404 failures — the main class must be at the root package that is the ancestor of all controllers, services, and repositories.
  • Never rely on default ddl-auto settings — explicitly set spring.jpa.hibernate.ddl-auto in every environment-specific properties file. Use validate or none in production and manage schema changes with Flyway or Liquibase.

⚠ Common Mistakes to Avoid

    Placing the main class in a non-parent package relative to controllers and services
    Symptom

    Every HTTP endpoint returns 404 with NoHandlerFoundException. No error in the logs — the controllers are correctly annotated, the application starts without errors, but Spring never discovered the controllers because @ComponentScan only searched the main class's package and its sub-packages. Sibling packages and unrelated package trees are invisible to the default scan.

    Fix

    Always put the @SpringBootApplication class at the root package that is the ancestor of all other packages in your application — for example, io.thecodeforge.bookstore as the parent of io.thecodeforge.bookstore.controller, io.thecodeforge.bookstore.service, and io.thecodeforge.bookstore.repository. If your code genuinely spans multiple package trees, add @ComponentScan(basePackages = {"io.thecodeforge", "com.company"}) explicitly to declare all roots.

    Using @Autowired on fields instead of constructor injection
    Symptom

    Unit tests fail with NullPointerException on the field because the @Autowired field is null without a running Spring context — Mockito's @InjectMocks does not reliably inject into private fields. The class cannot be instantiated outside a Spring context, which makes pure unit testing impossible without additional test infrastructure. Circular dependency bugs are hidden until runtime because Spring works around them with CGLIB proxies instead of failing at construction.

    Fix

    Use constructor injection with final fields for all mandatory dependencies. The constructor makes every dependency explicit — the compiler enforces that all required dependencies are provided, mocks can be passed directly to the constructor in tests (new BookService(mockRepository)), and final fields guarantee immutability after construction. If a class has so many constructor parameters that it feels awkward, that is a signal to split the class, not a reason to use field injection.

    Not explicitly setting spring.jpa.hibernate.ddl-auto per environment
    Symptom

    Application works perfectly with H2 locally using the create-drop default — schema is rebuilt on every restart, which is convenient. When deployed to PostgreSQL in staging or production, the same behavior applies to a database with real data. Every application restart drops all tables, recreating an empty schema. Data is permanently lost without any warning or error log entry — Hibernate logs the DROP TABLE statements at INFO level if Hibernate SQL logging is enabled, but not otherwise.

    Fix

    Explicitly set spring.jpa.hibernate.ddl-auto in every environment-specific properties file. Use create or create-drop in application-dev.properties for local development with disposable data. Use validate in application-prod.properties for production — this checks that the existing schema matches entity definitions without modifying anything. Manage actual schema changes through Flyway or Liquibase versioned SQL migration files checked into source control.

    Not running with --debug to understand what auto-configuration activated
    Symptom

    Unexpected beans appear in the context — a DataSource is created for a database that does not exist, causing startup failure. Or expected beans are missing — JdbcTemplate is not available even though the datasource is configured. Hours are spent manually tracing which auto-configuration class is responsible when the Conditions Evaluation Report would have shown the exact reason in seconds.

    Fix

    Run java -jar myapp.jar --debug or set logging.level.org.springframework.boot.autoconfigure=DEBUG at least once during the initial setup of any new service. Read the CONDITIONS EVALUATION REPORT — Positive matches show what auto-configuration activated, Negative matches show what was skipped and the exact condition that caused the skip. Make this a standard step in the new-service checklist.

    Overriding individual library versions inside a starter dependency without documentation
    Symptom

    Application compiles successfully but fails at runtime with ClassNotFoundException, NoSuchMethodError, or subtle behavioral changes. The overridden library version is incompatible with another library that the BOM manages — they share an API that changed between versions. The incompatibility is not obvious at compile time and can take significant time to trace back to the version override.

    Fix

    Resist overriding individual versions unless you have a specific, documented reason such as a security patch for a CVE. Use the BOM property names where they exist — Spring Boot's parent POM exposes version properties like jackson.version and hibernate.version that can be overridden in the project's properties section. Add a comment documenting why the override exists and when it can be removed. Test thoroughly with the integration test suite after any version override.

Interview Questions on This Topic

  • QWhat is the internal difference between @SpringBootApplication and @EnableAutoConfiguration? Under what specific scenario would you define them separately?Mid-levelReveal
    @SpringBootApplication is a composed meta-annotation that combines three annotations: @SpringBootConfiguration (a specialization of @Configuration that marks the class as a primary configuration source), @EnableAutoConfiguration (the auto-configuration trigger), and @ComponentScan (which scans the current package and sub-packages for stereotyped components). @EnableAutoConfiguration is specifically responsible for importing AutoConfigurationImportSelector, which reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports from every JAR on the classpath and evaluates each listed configuration class against its @ConditionalOn guards. You would break @SpringBootApplication into its constituent annotations when you need custom component scan behavior that the composed annotation cannot express. The most common case: your application code spans multiple package trees that are not in a parent-child relationship. If @SpringBootApplication is in io.thecodeforge.app and controllers are in com.company.controllers, the default @ComponentScan misses the controllers entirely. Breaking the annotations apart lets you write @ComponentScan(basePackages = {"io.thecodeforge", "com.company"}) explicitly while keeping @EnableAutoConfiguration for auto-configuration. Another case: a test configuration class that should not trigger component scanning at all — you use @EnableAutoConfiguration alone without @ComponentScan to avoid picking up production beans in the test context.
  • QExplain the Starters mechanism in Spring Boot. How does Maven manage transitive dependencies when you add spring-boot-starter-web?JuniorReveal
    Starters are curated dependency descriptor POM files that declare a set of related libraries with tested, compatible versions managed by the spring-boot-dependencies BOM. When you add spring-boot-starter-web to your pom.xml, Maven resolves its transitive dependency tree: spring-web, spring-webmvc, spring-boot-starter (the core starter), spring-boot-starter-tomcat (which itself pulls in embedded Tomcat), spring-boot-starter-json (which pulls in Jackson), and all of their transitive dependencies. The exact version of each library is managed by the spring-boot-dependencies BOM inherited through spring-boot-starter-parent — you never specify a version for any of these because the BOM has already determined the compatible combination. The problem this solves is 'dependency hell' — the classical scenario where Library A requires Jackson 2.14 and Library B requires Jackson 2.16, and you have to manually determine which version satisfies both or restructure your dependencies. With starters and the BOM, Spring Boot has already done this compatibility testing. The tradeoff is that the BOM has opinions about versions. When you need to override a version for a security patch, you step outside the tested matrix and take on the responsibility of verifying compatibility yourself.
  • QIf you define a bean of the same type as one provided by Spring Boot Auto-Configuration, what determines which bean wins? Explain @ConditionalOnMissingBean.Mid-levelReveal
    Your explicitly defined bean always wins. The auto-configuration classes in Spring Boot's spring-boot-autoconfigure module apply @ConditionalOnMissingBean to their @Bean methods — this annotation instructs Spring to only create the bean if no bean of that type (or that name, depending on configuration) already exists in the ApplicationContext. When you define your own @Bean method returning, say, an ObjectMapper, Spring evaluates JacksonAutoConfiguration's conditions and finds that a bean of type ObjectMapper already exists in the context. The @ConditionalOnMissingBean condition fails and Jackson's auto-configured ObjectMapper is never created. Your bean is the only ObjectMapper in the context. You can verify this by running with --debug and looking for JacksonAutoConfiguration in the Conditions Evaluation Report under Negative matches — it will show 'did not match: ConditionalOnMissingBean found existing bean of type com.fasterxml.jackson.databind.ObjectMapper.' This is the 'opinionated but overridable' principle made concrete: the framework provides sensible defaults but uses @ConditionalOnMissingBean throughout to ensure user-defined beans always take precedence.
  • QHow do you implement an external configuration pattern in Spring Boot to ensure credentials are never committed to version control?Mid-levelReveal
    Use the ${ENV_VAR_NAME} placeholder pattern in application.properties — for example, spring.datasource.password=${DB_PASSWORD}. The actual credential value is never stored in the properties file. At runtime, Spring resolves the placeholder against the environment, which includes system environment variables, JVM system properties, and other property sources in a defined priority order. The properties file contains only the placeholder and can be safely committed to version control. For a more structured approach, use @ConfigurationProperties with @Validated to define typed configuration classes. This gives you compile-time type checking, IDE autocomplete, and JSR-303 validation that fails at startup if a required property is missing — rather than failing at the point of first use. The application fails loud and early with a clear message pointing to the exact missing property. For production deployments, the actual secret values live in Kubernetes Secrets, AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager — injected into the container environment at runtime. Spring Cloud Config Server is the option for teams that need centralized configuration management across many services. The key principle across all approaches: no credential value ever touches a file that could reach version control, including Dockerfiles, docker-compose.yml, and CI/CD pipeline definitions.
  • QWhat is the Fat JAR layout? How does Spring Boot's custom classloader handle nested JARs?SeniorReveal
    A Fat JAR — also called an Executable JAR or Uber JAR — is a single self-contained artifact produced by the spring-boot-maven-plugin's repackage goal. The layout consists of three sections: META-INF/MANIFEST.MF, which specifies JarLauncher as the Main-Class (the Spring Boot launcher, not your application class) and Start-Class pointing to your @SpringBootApplication class; BOOT-INF/classes/, which contains your compiled application bytecode and resources; and BOOT-INF/lib/, which contains all dependency JARs nested inside the Fat JAR as actual JAR files. The challenge is that standard Java classloaders — URLClassLoader — cannot load classes from JARs nested inside other JARs. The ZIP specification allows nesting, but the classloader does not support it natively. Spring Boot solves this with LaunchedURLClassLoader, a custom classloader that understands the BOOT-INF/lib/ layout. JarLauncher's main method creates this custom classloader, sets up the classpath from the nested JARs in BOOT-INF/lib/, and then invokes your application's main method in a thread that uses the custom classloader. From your application code's perspective, all dependencies are on the classpath as normal — the layered classloader is transparent. Spring Boot also supports layered JARs — splitting the Fat JAR into distinct layers (dependencies, Spring Boot loader infrastructure, snapshot dependencies, application code) for more efficient Docker image builds. Only the layer that changes needs to be rebuilt and pushed, rather than the entire Fat JAR.

Frequently Asked Questions

Do I need to know the Spring Framework before learning Spring Boot?

You can be productive with Spring Boot without deep Spring knowledge — the defaults handle enough of the infrastructure that you can build real applications quickly. However, when things go wrong (and they will), understanding Spring's underlying concepts — the ApplicationContext, bean lifecycle, dependency injection, and AOP — is what separates a five-minute fix from a five-hour debugging session. Spring Boot is the dashboard; Spring is the engine. You can drive without understanding the engine, but you cannot diagnose problems without it. I recommend spending at least one week working through raw Spring concepts before relying entirely on Spring Boot's conventions — it makes every subsequent Spring Boot concept click faster.

What is the difference between Spring Boot and Spring MVC?

Spring MVC is a web framework inside the Spring ecosystem that handles HTTP request routing, controller dispatching, view resolution, and response rendering. It is a component of Spring, not a standalone product. Spring Boot is a bootstrapping and configuration tool that can include Spring MVC as one of its features — when you add spring-boot-starter-web, Spring Boot auto-configures Spring MVC for you. The two are not alternatives. Spring MVC handles the web layer; Spring Boot handles the setup of Spring MVC (and everything else). When you write @RestController, you are using Spring MVC. When that controller is discovered without writing a dispatcher servlet configuration, that is Spring Boot.

Why does Spring Boot embed Tomcat instead of requiring an external server?

Embedding the server is an intentional architectural decision with several practical consequences. It makes your application a self-contained deployable artifact — one JAR that runs anywhere Java is installed, with no external server to configure, version-manage, or maintain separately from the application. This is essential for containerized deployments where the environment is ephemeral and you want deterministic behavior: the same Tomcat version, the same configuration, every time. It also eliminates the 'works on staging, fails on production' class of bugs caused by different Tomcat versions between environments. External server deployment is still supported via WAR packaging if your organization requires it — packaging WAR instead of JAR and extending SpringBootServletInitializer gives you a deployable artifact for traditional application servers.

How do I handle database schema migrations in a production Spring Boot application?

Flyway and Liquibase are the two standard tools, and both integrate with Spring Boot out of the box. Flyway is the simpler option for teams that prefer plain SQL — you create numbered migration files (V1__create_books_table.sql, V2__add_price_column.sql) in src/main/resources/db/migration/ and Spring Boot runs them automatically at startup. Each migration is tracked in a schema_history table so Flyway knows which have been applied. Liquibase supports XML, YAML, and SQL formats and offers more advanced features like rollback scripts. Both tools treat your database schema as code with version history, code review, and controlled rollback — the same discipline you apply to application code. Set spring.jpa.hibernate.ddl-auto=validate alongside either tool so Hibernate verifies schema correctness at startup without ever modifying it.

How do I debug why a specific auto-configuration class did not activate?

Run the application with java -jar myapp.jar --debug or add logging.level.org.springframework.boot.autoconfigure=DEBUG to application.properties. This prints the CONDITIONS EVALUATION REPORT at startup. Find the auto-configuration class you expected to activate — it will be in the Negative matches section if it was skipped. The entry shows the exact @Conditional annotation that failed and what value it evaluated against. The three most common reasons: @ConditionalOnClass failed because a required library JAR is not on the classpath (add the missing starter or dependency), @ConditionalOnMissingBean triggered because a bean of that type already exists in the context (your explicit bean is in control, which may be intentional), or @ConditionalOnProperty failed because the required property is not set or has a different value than the havingValue attribute expected. Read the report before investigating anything else — it tells you the exact reason in plain text.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousJava Memory Leaks and PreventionNext →Maven vs Gradle in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged