Spring Boot Introduction: Why It Exists and How to Build Your First App
Every Java developer hits the same wall. You have a great idea for a web service, but before writing a single line of business logic you're buried in XML files, manually wiring beans, configuring a servlet container, and fighting dependency version mismatches. By the time your environment works, you've forgotten what you were building. Spring Boot was created specifically to eliminate that wall — and it's why almost every new Java backend project in the industry starts with it today.
The core problem Spring Boot solves is bootstrapping friction. The original Spring Framework is powerful but famously verbose. A simple REST endpoint could require a web.xml, a Spring MVC dispatcher config, a context configuration class, and a dozen explicit bean definitions. Spring Boot replaces all of that with intelligent defaults and auto-configuration — it looks at what's on your classpath and wires things up for you automatically. You bring the feature; Spring Boot brings the scaffolding.
By the end of this article you'll understand exactly what Spring Boot is (and isn't), why auto-configuration is the secret engine behind it, how to build and run a real REST API in minutes, and where the common traps are that catch even experienced developers off guard. You'll be ready to explain it confidently in an interview and use it correctly in production.
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. Spring Boot is a launcher and opinion layer built on top of Spring. It uses the same Spring core — dependency injection, Spring MVC, Spring Data — but makes opinionated decisions about how those pieces fit together so you don't have to.
Three pillars hold up everything Spring Boot does:
- Auto-Configuration — Spring Boot scans your classpath and automatically creates beans you'd otherwise define manually. If it sees
spring-boot-starter-webon the classpath, it auto-configures an embedded Tomcat server, a DispatcherServlet, and Jackson for JSON serialization. No XML. No boilerplate.
- Starter Dependencies — Instead of hunting for compatible Maven/Gradle versions of 12 different Spring libraries, you add one starter like
spring-boot-starter-data-jpaand it pulls in everything that works together, versioned correctly.
- Embedded Server — Your application ships as a single executable JAR with an embedded Tomcat (or Jetty/Undertow). No deploying WAR files to an external server. You run
java -jar myapp.jarand you're live.
The key mental model: Spring Boot doesn't add features to Spring — it removes the work of configuring them.
package io.thecodeforge.bookstore; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @SpringBootApplication is a convenience annotation that combines three annotations: * 1. @Configuration — marks this class as a source of bean definitions * 2. @EnableAutoConfiguration — tells Spring Boot to start auto-configuring beans * based on what's on the classpath * 3. @ComponentScan — tells Spring to scan this package (and sub-packages) * for @Component, @Service, @Repository, @Controller etc. * * You almost never split these three apart in a real project. */ @SpringBootApplication public class BookstoreApplication { public static void main(String[] args) { // SpringApplication.run() does the heavy lifting: // - Creates the ApplicationContext // - Registers all auto-configured beans // - Starts the embedded Tomcat server on port 8080 SpringApplication.run(BookstoreApplication.class, args); } }
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.0)
2024-01-15T10:23:41.123Z INFO --- [main] i.t.bookstore.BookstoreApplication : Starting BookstoreApplication
2024-01-15T10:23:42.456Z INFO --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080
2024-01-15T10:23:42.461Z INFO --- [main] i.t.bookstore.BookstoreApplication : Started BookstoreApplication in 1.847 seconds
Auto-Configuration: The Engine Under the Hood
Auto-configuration is the most powerful — and most misunderstood — feature in Spring Boot. When your app starts, Spring Boot runs through a list of candidate configuration classes defined in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Each one is guarded by @ConditionalOn* annotations that only activate the configuration if certain conditions are true.
For example, DataSourceAutoConfiguration only fires if there's a JDBC driver on the classpath AND no DataSource bean has been manually defined. This conditional logic is the key: auto-configuration never overwrites your explicit choices. It fills in the gaps.
This is why Spring Boot is described as 'opinionated but overridable.' You can override any auto-configured bean by simply declaring your own. Want a custom Jackson ObjectMapper? Define one as a @Bean and Spring Boot's JacksonAutoConfiguration backs off automatically.
Understanding this conditional model is what separates developers who use Spring Boot from developers who truly understand it. When something isn't working the way you expect, the first question to ask is: 'Is auto-configuration being triggered or not?' Run your app with --debug flag to get a full auto-configuration report printed to the console.
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; /** * @RestController = @Controller + @ResponseBody * Every method return value is automatically serialized to JSON by * Jackson — which was auto-configured because spring-boot-starter-web * is on the classpath. You didn't configure Jackson. Spring Boot did. */ @RestController @RequestMapping("/api/books") // All endpoints in this class are prefixed with /api/books public class BookController { // Spring injects BookService automatically via constructor injection. // This is the preferred injection style — it makes the dependency explicit // and the class easy to unit test without Spring. private final BookService bookService; public BookController(BookService bookService) { this.bookService = bookService; } /** * GET /api/books — returns all books as a JSON array. * ResponseEntity gives us full control over the HTTP status code. */ @GetMapping public ResponseEntity<List<Book>> getAllBooks() { List<Book> books = bookService.findAllBooks(); return ResponseEntity.ok(books); // 200 OK + JSON body } /** * POST /api/books — creates a new book. * @RequestBody tells Spring to deserialize the incoming JSON into a Book object. * @ResponseStatus sets the default HTTP response code to 201 Created. */ @PostMapping @ResponseStatus(HttpStatus.CREATED) // Returns 201 instead of default 200 public Book createBook(@RequestBody Book newBook) { return bookService.saveBook(newBook); // Returns saved book with generated ID } /** * GET /api/books/{id} — find a specific book by its ID. * Returns 404 if not found — handled cleanly via Optional. */ @GetMapping("/{bookId}") public ResponseEntity<Book> getBookById(@PathVariable Long bookId) { return bookService.findBookById(bookId) .map(ResponseEntity::ok) // Found: 200 OK with book .orElse(ResponseEntity.notFound().build()); // Not found: 404 } }
HTTP 201 Created
{
"id": 1,
"title": "Clean Code",
"author": "Robert Martin",
"price": 29.99
}
# After GET /api/books
HTTP 200 OK
[
{
"id": 1,
"title": "Clean Code",
"author": "Robert Martin",
"price": 29.99
}
]
# After GET /api/books/999
HTTP 404 Not Found
Building the Complete Bookstore: Service, Model and application.properties
A controller without a service and model isn't a real app — it's a demo. Let's complete the picture. The service layer is where your business logic lives. The model defines your data shape. And application.properties (or application.yml) is where Spring Boot externalizes configuration — port numbers, database URLs, custom properties — all without touching code.
This separation of concerns is not just good practice; it's what makes Spring Boot apps testable and maintainable at scale. You can swap out the BookService implementation for a mock in tests, change the database from H2 to PostgreSQL by editing a single properties file, and change the server port for different environments without recompiling.
Notice how the Book model below uses @Entity and @Id — this wires into Spring Data JPA, which is itself auto-configured when you add spring-boot-starter-data-jpa. The pattern repeats: add a starter, write your domain code, Spring Boot wires the infrastructure.
// ───────────────────────────────────────────── // FILE 1: Book.java (the Model / Entity) // ───────────────────────────────────────────── package io.thecodeforge.bookstore.model; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; /** * @Entity marks this class as a JPA entity — Spring Data JPA will * automatically create a 'book' table in the database for this. * With H2 in-memory DB on the classpath, no SQL setup needed. */ @Entity @Table(name = "books") public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-increment primary key private Long id; @NotBlank(message = "Title cannot be empty") // Validated by @Valid in controller @Column(nullable = false) private String title; @NotBlank(message = "Author cannot be empty") private String author; @Positive(message = "Price must be greater than zero") private Double price; // JPA requires a no-arg constructor protected Book() {} public Book(String title, String author, Double price) { this.title = title; this.author = author; this.price = price; } // Getters and setters — needed for Jackson JSON serialization 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; } } // ───────────────────────────────────────────── // FILE 2: BookRepository.java (the Data Layer) // ───────────────────────────────────────────── package io.thecodeforge.bookstore.repository; import io.thecodeforge.bookstore.model.Book; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** * JpaRepository<Book, Long> gives us findById, findAll, save, delete etc. * for FREE — Spring Data generates the implementation at runtime. * We don't write a single SQL query for standard CRUD operations. */ @Repository public interface BookRepository extends JpaRepository<Book, Long> { // Spring Data can generate queries from method names alone: // findByAuthor(String author) → SELECT * FROM books WHERE author = ? // No SQL needed — the method name IS the query. } // ───────────────────────────────────────────── // FILE 3: BookService.java (the Business Layer) // ───────────────────────────────────────────── 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 is a specialization of @Component — it marks this class * as a service-layer bean and makes it eligible for component scanning. * Functionally identical to @Component, but communicates intent clearly. */ @Service public class BookService { private final BookRepository bookRepository; // Constructor injection — Spring automatically injects the BookRepository bean. // Because there's only ONE constructor, @Autowired is not required in Spring 4.3+. public BookService(BookRepository bookRepository) { this.bookRepository = bookRepository; } public List<Book> findAllBooks() { return bookRepository.findAll(); } public Optional<Book> findBookById(Long bookId) { return bookRepository.findById(bookId); } @Transactional // Wraps the DB operation in a transaction — rolls back on exception public Book saveBook(Book newBook) { return bookRepository.save(newBook); } } // ───────────────────────────────────────────── // FILE 4: application.properties // Location: src/main/resources/application.properties // ───────────────────────────────────────────── // server.port=8080 ← default, shown for clarity // spring.application.name=bookstore-api // // # H2 in-memory database (auto-configured when h2 is on classpath) // spring.datasource.url=jdbc:h2:mem:bookstoredb // spring.datasource.driver-class-name=org.h2.Driver // spring.h2.console.enabled=true ← access DB GUI at /h2-console // // # Show SQL queries in console during development // spring.jpa.show-sql=true // spring.jpa.hibernate.ddl-auto=create-drop ← creates schema on start, drops on stop
Tomcat started on port(s): 8080
Hibernate: create table books (id bigint generated by default as identity, author varchar(255), price float(53), title varchar(255) not null, primary key (id))
Started BookstoreApplication in 2.341 seconds
# POST /api/books
Hibernate: insert into books (author, price, title) values (?, ?, ?)
HTTP 201 → {"id":1,"title":"Clean Code","author":"Robert Martin","price":29.99}
# GET /api/books
Hibernate: select b1_0.id,b1_0.author,b1_0.price,b1_0.title from books b1_0
HTTP 200 → [{"id":1,"title":"Clean Code","author":"Robert Martin","price":29.99}]
| Feature / Aspect | Traditional Spring MVC | Spring Boot |
|---|---|---|
| Setup time for a REST endpoint | 30-60 min (XML + config classes) | Under 5 min (starter + annotation) |
| Server deployment | Deploy WAR to external Tomcat/JBoss | Run JAR directly — server is embedded |
| Dependency management | Manually match compatible versions | Starter POMs manage versions automatically |
| Configuration style | Mostly explicit XML or Java @Config | Convention-based with properties override |
| Auto-configuration | None — everything manually declared | Conditional auto-config based on classpath |
| Production readiness | Manual setup for metrics/health | Actuator adds /health, /metrics instantly |
| Learning curve | Steep — many moving parts | Gentle start, deep customization available |
| Best for | Legacy enterprise apps already on Spring | Any new Java backend project from scratch |
🎯 Key Takeaways
- Spring Boot is a launcher layer on top of Spring — it doesn't replace Spring, it removes the setup work. All the same Spring concepts still apply underneath.
- Auto-configuration is conditional — it only activates when its @ConditionalOn* conditions are met and always backs off when you define your own bean. Run with --debug to see exactly what fired and why.
- Constructor injection is the correct way to wire dependencies in Spring Boot — it makes your classes testable without a Spring context and makes dependencies visible and honest.
- The starter dependency model solves the version-mismatch problem by giving you a single, curated, tested set of compatible library versions — resist the urge to override individual versions unless you have a specific reason.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Placing the main class in a non-parent package — If your @SpringBootApplication class is in
io.thecodeforgebut your controllers are incom.mycompany.controllers, component scanning won't find them and you'll get 404s on every endpoint with no error. Fix: always put the main class at the root package that is the parent of all your other packages, e.g.,io.thecodeforge.bookstore. - ✕Mistake 2: Using @Autowired on fields instead of constructors — Field injection like
@Autowired private BookService bookServiceworks but hides dependencies, makes unit testing painful (you can't inject mocks without a Spring context), and hides circular dependency bugs until runtime. Fix: always use constructor injection. If a class has too many constructor parameters, that's a code smell telling you to split the class. - ✕Mistake 3: Forgetting that spring.jpa.hibernate.ddl-auto defaults to create-drop for embedded databases — Developers test locally with H2, everything works, they switch to PostgreSQL in production and suddenly the schema isn't being created and the app crashes immediately. Fix: explicitly set ddl-auto for every environment and use Flyway or Liquibase for schema migrations in staging and production environments.
Interview Questions on This Topic
- QWhat is the difference between @SpringBootApplication and @EnableAutoConfiguration — and when would you ever split them apart?
- QHow does Spring Boot auto-configuration know NOT to override a bean you've defined yourself — what mechanism prevents the conflict?
- QIf you add both spring-boot-starter-web and spring-boot-starter-webflux to the same project, what happens and why?
Frequently Asked Questions
Do I need to know the Spring Framework before learning Spring Boot?
Ideally yes, but you can be productive with Spring Boot without deep Spring knowledge. However, when things go wrong — and they will — understanding Spring's core concepts like the ApplicationContext, bean lifecycle, and dependency injection will save you hours of debugging. Think of Spring as the engine and Spring Boot as the dashboard.
What is the difference between Spring Boot and Spring MVC?
Spring MVC is a web framework inside the Spring ecosystem that handles HTTP requests, routing, and response rendering. Spring Boot is a bootstrapping tool that can use Spring MVC (via spring-boot-starter-web) as one of many possible features. Spring Boot sets up Spring MVC for you automatically — you use both at the same time, but they solve different problems.
Why does Spring Boot embed Tomcat? Isn't it better to use an external server?
Embedding the server is intentional and powerful. It makes your app a self-contained deployable unit — one JAR that runs anywhere Java runs. This is essential for containerized deployments (Docker, Kubernetes) where the environment is ephemeral and you want full control over the server version. External server deployment is still supported via WAR packaging if your organisation requires it.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.