Spring Boot Introduction: Why It Exists and How to Build Your First App
- 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.
- 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
Port already in use — application fails to bind to port 8080 on startup
lsof -i :8080kill -9 $(lsof -t -i :8080)Bean not found — NoSuchBeanDefinitionException at startup or NoHandlerFoundException for every endpoint
java -jar myapp.jar --debug 2>&1 | grep -A 3 'CONDITIONS EVALUATION'java -jar myapp.jar --debug 2>&1 | grep 'did not match'Application runs out of memory — OutOfMemoryError in logs or OOMKill in Kubernetes
jcmd $(pgrep -f spring-boot) GC.heap_dump /tmp/heapdump.hprofkubectl describe pod <pod-name> | grep -A 5 'Last State'Database connection refused on startup — ECONNREFUSED to localhost:5432 or data source URL error
docker ps | grep postgrespg_isready -h localhost -p 5432 -U forge_adminProduction Incident
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 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.
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); } }
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
:: 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)
- 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
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.
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); } }
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))
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.
// ── 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); } }
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}]
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.
<?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>
// 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)
| Feature / Aspect | Traditional Spring MVC | Spring Boot |
|---|---|---|
| Setup time for a working REST endpoint | 30 to 60 minutes — web.xml, dispatcher config, context config, explicit bean declarations all required before the first line of business logic | Under 5 minutes — starter dependency, @SpringBootApplication, @RestController, done |
| Server deployment model | Compile to WAR, deploy to external Tomcat or JBoss, manage server version separately from application version | Compile to Fat JAR, run with java -jar — server version is part of the application, consistent across every environment |
| Dependency management | Manually specify compatible versions for Spring core, Spring MVC, Jackson, validation API, Hibernate — version conflicts are common | Declare starters without versions — spring-boot-dependencies BOM manages compatible versions for the entire ecosystem |
| Configuration style | Mostly explicit XML or @Configuration Java classes — every bean, every mapping, every serializer declared manually | Convention-based with application.properties overrides — declare nothing for the common case, override specific settings when needed |
| Auto-configuration | None — every bean must be declared explicitly, nothing is inferred from the classpath | Conditional auto-configuration based on classpath, existing beans, and properties — the Conditions Evaluation Report makes every decision transparent |
| Production readiness | Manual setup required for health endpoints, metrics, and monitoring integration — each team implements differently | spring-boot-starter-actuator adds /actuator/health, /actuator/metrics, and /actuator/loggers instantly — Kubernetes-compatible probes out of the box |
| Learning curve | Steep — many moving parts that must be understood and wired together before anything works | Gentle entry point, same Spring depth available — you can be productive quickly and go deep when you need to |
| Best for | Maintaining existing Spring applications already running in production on external servers with established deployment pipelines | Any 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
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
- QExplain the Starters mechanism in Spring Boot. How does Maven manage transitive dependencies when you add spring-boot-starter-web?JuniorReveal
- 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
- QHow do you implement an external configuration pattern in Spring Boot to ensure credentials are never committed to version control?Mid-levelReveal
- QWhat is the Fat JAR layout? How does Spring Boot's custom classloader handle nested JARs?SeniorReveal
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.
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.