Java Framework

Spring Boot

Spring Boot is the opinionated layer on top of the Spring Framework that turns a pile of configuration into a running application: starters, auto-configuration and an embedded server.

advanced16 min readUpdated Sep 16, 2026
PostController.java
java
// src/main/java/com/example/blog/PostController.java
@RestController
@RequestMapping("/api/posts")
public class PostController {

    private final PostService postService;

    public PostController(PostService postService) {
        this.postService = postService;
    }

    @GetMapping
    public List<PostResponse> list() {
        return postService.findAll();
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public PostResponse create(@Valid @RequestBody CreatePostRequest request) {
        return postService.create(request);
    }
}
Released
2014
Built on
Spring Framework
Language
Java, Kotlin, Groovy
Server
Embedded Tomcat, Jetty or Netty
Style
Convention and auto-configuration
Version
3.x

Why it matters

Why Spring Boot dominates the JVM

Running in minutes

A starter and one main method give you a production-shaped application with an embedded server, so there is nothing to deploy before you write code.

Auto-configuration

Spring Boot inspects the classpath and configures the beans it finds. Add a database driver and a DataSource appears, ready to use.

One coherent ecosystem

Spring Data, Security, Batch, Integration and Cloud all share the same programming model, so skills transfer across the whole platform.

The big picture

Auto-config, DI, starters

Starters pull in coherent dependencies, auto-configuration wires them from sensible defaults, and the container injects the objects your classes need.

Auto-configuration

Bootstrap

Conditional configuration reads what is on the classpath and supplies defaults, which you override with properties rather than code.

Annotated web layer

Expose

Annotations map HTTP methods and paths to Java methods, and message converters turn objects into JSON automatically.

Spring Data JPA

Persist

Repository interfaces give you CRUD, query methods and paging without hand-written implementations.

At a glance

The Spring Boot toolbox

Embedded server

Tomcat, Jetty or Netty runs inside the process as a plain Java library.

Starters

spring-boot-starter-web pulls in the web stack in a single dependency.

Annotations

@RestController, @GetMapping and @RequestBody describe the API.

Dependency injection

The application context creates and wires beans via constructor injection.

Spring Data JPA

Repository interfaces replace boilerplate data access code.

Actuator

Health, metrics and info endpoints are one dependency away.

A short history

From Spring XML to a one-line main

  1. 2012

    Spring Boot is announced

    The Spring team previews a way to bootstrap applications without the XML that had grown around the framework.

    12
  2. 2014

    Spring Boot 1.0

    The first GA release makes starters, auto-configuration and an embedded server the standard way to build Spring apps.

    14
  3. 2018

    Spring Boot 2.0

    Spring 5 brings WebFlux, a rewritten Actuator and first-class Kotlin support.

    18
  4. 2022

    Spring Boot 3.0

    Java 17 becomes the baseline, the jakarta namespace replaces javax, and GraalVM native images mature.

    22
  5. 2024

    Spring Boot 3.4

    Structured logging, improved observability and updated dependency baselines keep the platform current.

    24
  6. Today

    The default JVM stack

    Spring Boot is the assumed starting point for new Java services, from monoliths to cloud-native microservices.

    Today

The complete guide

Spring Boot: Everything you need to know

What is Spring Boot?

Spring Boot is a layer on top of the Spring Framework that removes the setup Spring once required. Instead of XML files, servlet configuration and a container to deploy into, you write a class with a main method, add a starter dependency, and get a running application with an embedded server.

Three ideas do the heavy lifting. Starters bundle the dependencies for a capability into one line. Auto-configuration inspects the classpath and configures beans from sensible defaults. The embedded server means the application is a normal Java process — there is nothing to install or deploy into.

The result is that Spring Boot has become the default way to build JVM services, from small internal APIs to large microservice fleets, while keeping the full power of the Spring ecosystem underneath.

The Spring ecosystem in one minute

Spring is not one library but a family that shares a programming model:

  • Spring Framework — the core container, dependency injection and the web layer.
  • Spring Data — repositories over SQL and NoSQL stores.
  • Spring Security — authentication and authorisation.
  • Spring Batch / Integration — bulk processing and messaging.
  • Spring Cloud — configuration, discovery and resilience for distributed systems.

Because they are designed together, a project can adopt them one at a time without changing how it is written. That consistency is the real reason teams standardise on Spring.

Starters and dependency management

A starter is a curated set of dependencies that work together. You declare the capability, not the individual jars.

<!-- pom.xml -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

The Gradle equivalent is just as short:

dependencies {
  implementation "org.springframework.boot:spring-boot-starter-web"
  implementation "org.springframework.boot:spring-boot-starter-data-jpa"
  implementation "org.springframework.boot:spring-boot-starter-validation"
  runtimeOnly "org.postgresql:postgresql"
}

The parent POM or the dependency-management plugin fixes compatible versions for you. Override a version only when you have a reason, and let the platform manage the rest.

The application class and embedded server

Every Spring Boot application starts here. This one class is enough to launch a web server.

package com.example.blog;

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

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

@SpringBootApplication combines three annotations: @Configuration, @EnableAutoConfiguration and @ComponentScan. Component scanning starts in the package of this class and looks downward, which is why the main class belongs at the root of your package structure.

Running ./mvnw spring-boot:run starts an embedded Tomcat on port 8080. You can swap in Jetty or Netty with a dependency change, and the application code does not care.

The annotated web layer

Spring MVC maps HTTP requests to methods with annotations. @RestController combines @Controller and @ResponseBody, so returned objects are serialised to JSON by Jackson.

@RestController
@RequestMapping("/api/posts")
public class PostController {

    private final PostService service;

    public PostController(PostService service) {
        this.service = service;
    }

    @GetMapping
    public List<PostResponse> list(
            @RequestParam(defaultValue = "0") int page) {
        return service.findPage(page);
    }

    @GetMapping("/{id}")
    public PostResponse get(@PathVariable Long id) {
        return service.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public PostResponse create(@Valid @RequestBody CreatePostRequest request) {
        return service.create(request);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        service.delete(id);
    }
}

@PathVariable reads from the URL, @RequestParam from the query string, and @RequestBody deserialises the payload. Keep controllers thin: they translate HTTP into a call on a service and translate the result back. Domain logic belongs in the service layer.

Dependency injection and the application context

The application context is the container that creates objects, called beans, and supplies their dependencies. You declare what a class needs; Spring decides how to build it.

@Service
public class PostService {

    private final PostRepository repository;
    private final Clock clock;

    public PostService(PostRepository repository, Clock clock) {
        this.repository = repository;
        this.clock = clock;
    }

    @Transactional
    public PostResponse create(CreatePostRequest request) {
        var post = new Post(request.title(), request.body(), clock.instant());
        return PostResponse.from(repository.save(post));
    }
}

Stereotype annotations mark the classes to manage: @Component for generic beans, @Service for domain services, @Repository for data access and @RestController for web endpoints. For anything you cannot annotate — a third-party class, a configured client — declare it in a @Configuration class.

@Configuration
class ClockConfig {

    @Bean
    Clock clock() {
        return Clock.systemUTC();
    }
}

Prefer constructor injection everywhere. It makes dependencies explicit, allows final fields and lets you instantiate the class in a plain unit test without starting Spring.

Spring Data JPA and the database

Entities map Java objects to tables, and repositories give you data access without an implementation.

@Entity
@Table(name = "posts")
public class Post {

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

    @Column(nullable = false, length = 120)
    private String title;

    @Column(nullable = false)
    private String body;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Author author;

    protected Post() {
    }

    public Post(String title, String body, Author author) {
        this.title = title;
        this.body = body;
        this.author = author;
    }

    // getters and domain methods
}
public interface PostRepository extends JpaRepository<Post, Long> {

    List<Post> findByPublishedTrueOrderByCreatedAtDesc();

    @Query("select p from Post p join fetch p.author where p.id = :id")
    Optional<Post> findWithAuthor(Long id);
}

Spring Data derives a query from the method name, so findByPublishedTrueOrderByCreatedAtDesc needs no body. Use @Query when the derived name would be unreadable, and join fetch to avoid the lazy-loading N+1 problem. Keep ddl-auto at validate in production and manage the schema with Flyway or Liquibase.

Configuration with application.yml and profiles

Configuration lives in src/main/resources and can be overridden by environment variables, which is what makes the same jar run everywhere.

# src/main/resources/application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/blog
    username: blog
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate
    open-in-view: false
server:
  port: 8080

Profiles let one build behave differently per environment.

# src/main/resources/application-prod.yml
spring:
  datasource:
    url: ${DATABASE_URL}
logging:
  level:
    root: warn

Activate a profile with --spring.profiles.active=prod or the SPRING_PROFILES_ACTIVE environment variable. @ConfigurationProperties binds a group of settings to a typed class, which is safer than scattering @Value annotations through the codebase.

Validation

Jakarta Bean Validation constraints on a request object are enforced when the controller parameter is annotated with @Valid.

public record CreatePostRequest(
        @NotBlank @Size(max = 120) String title,
        @NotBlank String body
) {
}

A @RestControllerAdvice centralises the error response so every endpoint returns the same shape.

@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> onValidation(MethodArgumentNotValidException ex) {
        return ex.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.toMap(
                        FieldError::getField,
                        FieldError::getDefaultMessage,
                        (first, second) -> first));
    }
}

Validate at the boundary and let the domain trust its inputs. Combined with a consistent exception handler, this removes repetitive if (input == null) checks from every service.

Spring Security in brief

Spring Security is a filter chain in front of your application. In modern Spring Boot you configure it with a bean rather than XML.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/actuator/health").permitAll()
                        .anyRequest().authenticated())
                .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
        return http.build();
    }
}

That single bean requires a valid JWT on every endpoint except the health check. Method-level rules with @PreAuthorize add fine-grained authorisation where it is needed. Security is deep — plan to read the documentation carefully before exposing anything to the internet.

Actuator: health and metrics

Adding the Actuator starter exposes operational endpoints for free.

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: always
curl localhost:8080/actuator/health

/actuator/health integrates with liveness and readiness probes, and /actuator/metrics reports JVM, HTTP and datasource statistics. Expose only what you need, and secure the rest — these endpoints reveal a lot about a running system.

Testing

@SpringBootTest starts the application context and is the base for integration tests. MockMvc exercises the web layer without opening a real port.

@SpringBootTest
@AutoConfigureMockMvc
class PostControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void listsPosts() throws Exception {
        mockMvc.perform(get("/api/posts"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].title").value("Hello"));
    }
}

For faster feedback, slice the context to just the layer under test and mock the collaborators.

@WebMvcTest(PostController.class)
class PostControllerSliceTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private PostService service;

    @Test
    void returnsNotFound() throws Exception {
        given(service.findById(1L)).willThrow(new PostNotFoundException(1L));
        mockMvc.perform(get("/api/posts/1"))
                .andExpect(status().isNotFound());
    }
}

Use slices for controllers and repositories, and reserve the full context for a few end-to-end tests. Testcontainers gives you a real database in tests without mocking persistence.

Building and deploying

The build produces an executable fat jar that contains your classes, dependencies and the embedded server.

./mvnw clean package
java -jar target/blog-0.0.1-SNAPSHOT.jar

For containers, Buildpacks create an optimised image without a Dockerfile.

./mvnw spring-boot:build-image

Configuration comes from the environment, so the same artifact moves from staging to production unchanged. Add a Dockerfile only when you need control Buildpacks do not offer, and prefer a multi-stage build to keep the image small.

Best practices

  • Package by feature so each domain owns its controller, service and repository.
  • Use constructor injection and keep fields final.
  • Return DTOs from controllers instead of JPA entities.
  • Validate request objects with @Valid and handle errors in one @RestControllerAdvice.
  • Keep ddl-auto at validate and manage schema changes with Flyway or Liquibase.
  • Externalise configuration and use profiles instead of rebuilding for each environment.
  • Add Actuator, then expose and secure only the endpoints you actually need.

Common mistakes

  • Field injection with @Autowired, which hides dependencies and complicates testing.
  • Serialising entities directly and leaking lazy-loading or internal columns.
  • Letting controllers grow business logic instead of delegating to services.
  • Using ddl-auto: update in production and drifting from the real schema.
  • Starting the whole context in every unit test, making the suite slow.
  • Exposing every Actuator endpoint to the public internet.
  • Adding starters you do not need and paying for them at startup.

Where to go next

Spring Boot teaches dependency injection, auto-configuration and layered design — ideas that appear far beyond Java. If you like the module-and-injection model, NestJS applies it in TypeScript, while Express shows the minimal alternative. The annotated web layer is ultimately a REST API, and most applications persist to a relational store, so PostgreSQL is the natural next step.

Injecting dependencies

Constructor injection makes required collaborators explicit, keeps fields final and lets you test the class without starting Spring.

Prefer
@Service
public class PostService {

    private final PostRepository repository;

    public PostService(PostRepository repository) {
        this.repository = repository;
    }
}
Avoid
@Service
public class PostService {

    @Autowired
    private PostRepository repository;
    // hidden dependency, hard to test in isolation
}

Returning data from controllers

Exposing JPA entities couples your API to the schema. Map to a DTO so lazy fields, internal columns and future migrations stay private.

Prefer
@GetMapping("/{id}")
public PostResponse get(@PathVariable Long id) {
    return service.findById(id);
}
Avoid
@GetMapping("/{id}")
public Post get(@PathVariable Long id) {
    return repository.findById(id).orElseThrow();
    // serialising the entity leaks the schema
}

Trade-offs

Is Spring Boot worth the weight?

Spring Boot rewards teams that want a proven, integrated platform. The cost is startup time, memory and a large surface area to learn.

Strengths

  • Everything is already integrated

    Web, data, security, validation, messaging and observability share one configuration model, so you rarely glue libraries together yourself.

  • The ecosystem and hiring pool are enormous

    It is the default enterprise Java stack. Libraries target it first, and finding experienced developers is easy.

  • Production features are first-class

    Actuator, externalised configuration, profiles and metrics treat operations as a design concern, not an afterthought.

Trade-offs

  • Startup and memory cost

    A Spring Boot service starts slower and uses more memory than a minimal framework. On serverless or many small services that overhead is real.

  • The learning curve is steep

    Auto-configuration hides a lot. When the default is wrong you need to understand the underlying Spring context to debug it.

  • Annotation magic can obscure flow

    Behaviour is spread across annotations, aspects and proxies. Following a request end to end requires practice and good tooling.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Spring Boot?

Our interactive tutorial walks you through Spring Boot step by step — with quizzes and real code you can run in the browser.