Spring Boot

Spring Boot assembles production-ready Spring applications from explicit dependencies, conditional defaults, and external configuration.

level intermediate time 12 min at Standard depth
version Spring Boot 4.1.1 / Java 25 LTS
what

Spring Boot creates stand-alone Spring applications by combining the Spring container with conditional configuration, curated dependencies, an embedded server, and operational support.

when

Use it when a Java service benefits from Spring’s dependency injection and ecosystem and the team wants explicit application code without manually assembling every framework integration.

how

Start with only the required starters, put the application class above the packages it should scan, bind and validate configuration, then test the context and the real HTTP boundary.

What it is and why it exists

Spring Boot is an application framework built on the Spring Framework. Spring supplies the container, dependency injection, web stack, transactions, and other programming models; Boot decides how to assemble common combinations from the libraries and settings present in an application. The result is still a Spring application, not a separate replacement for Spring.

The problem it solves is integration setup. A servlet API, JSON mapper, validation provider, server, logging system, and metrics registry each have their own lifecycle and configuration. Boot supplies conditional defaults for known combinations, while leaving application code free to replace specific parts. This is more precise than saying that Boot “requires no configuration.”

Auto-configuration contributes configuration when its conditions match. A servlet web application appears when the required web classes are on the classpath; a default component can back away when the application defines its own Spring bean . The conditions make defaults contextual rather than universal.

A starter dependency is a curated dependency descriptor for one capability, such as Spring MVC or Actuator. It keeps a compatible group of libraries together and lets Boot manage their versions. A starter is not a module switch: classpath contents make configurations eligible, while conditions and properties decide what is actually created.

Boot applications run through an application context , the Spring container that owns bean definitions and instances. Dependency injection connects those instances, normally through constructors. Controllers, services, configuration objects, and infrastructure adapters therefore share one explicit object graph.

Boot also turns configuration into deployment input. Externalized configuration can come from packaged files, profile-specific files, environment variables, system properties, command-line arguments, and other property sources. The application artifact can stay unchanged while ports, endpoints, credentials, and feature choices vary by environment.

You meet Spring Boot in HTTP services, batch jobs, message consumers, command-line applications, and larger modular systems. It is a good fit when Spring integrations reduce more work than the container and startup model add. A tiny function or service that needs no Spring features may have a simpler runtime and test boundary without it.

Spring Security, Spring Data, messaging, containers, and orchestration are separate concerns built on top of this core. Adding every starter to an initial project hides which dependency caused which behavior. Learn the context, conditions, configuration, web boundary, and test boundary first; add an integration only when a requirement names it.

How it works

SpringApplication.run() builds an environment, chooses an application-context type, loads bean definitions, refreshes the context, and starts the selected runtime. In a servlet application, the refresh creates the web infrastructure and embedded server. Startup either produces one coherent context or fails; a half-bound configuration object should not survive into request handling.

@SpringBootApplication is a convenience annotation combining @SpringBootConfiguration, @EnableAutoConfiguration, and @ComponentScan. The first identifies the primary configuration, the second imports Boot’s auto-configuration candidates, and the third discovers application components. Those three jobs are related but have different failure modes.

The package of the application class is the default search root. Placing it in com.example.catalog includes subpackages such as com.example.catalog.web and com.example.catalog.service. Putting it in a feature subpackage can silently omit siblings; putting it in the unnamed default package can make scanning unreasonably broad.

Auto-configuration candidates declare conditions over classes, beans, properties, resources, and application type. Boot evaluates those conditions while building the context. Matching configuration registers bean definitions; a failed condition explains why a configuration did not apply rather than representing an application error.

Defaults that back away

Boot’s defaults are non-invasive at the bean boundary. For example, an auto-configuration guarded by @ConditionalOnMissingBean contributes its default only if the relevant bean is absent. Defining an application bean can therefore replace one part without disabling unrelated web, JSON, or metrics configuration.

Back-off is type- and condition-specific. Adding a bean that looks conceptually similar does not prove that it satisfies the exact condition, and defining two beans of an injectable type may create an ambiguity instead of an override. Read the condition report and the configuration’s documented extension points before adding exclusions.

Run with --debug when startup behavior is surprising. Boot logs a conditions evaluation report that separates positive and negative matches. With Actuator deliberately exposed and secured, the conditions endpoint can provide related runtime evidence; it should not be made public merely for convenience.

Bean ownership and injection

A bean definition tells the context how to obtain an object and what lifecycle rules apply. The default scope is one instance per application context, often called singleton scope. It does not mean one instance across every context, test, or JVM, and it provides no automatic thread safety for mutable state.

Constructor injection makes required dependencies visible and lets the container reject an incomplete graph during startup. A single constructor needs no @Autowired. Field injection hides requirements until reflection fills them, makes ordinary construction awkward, and encourages tests that bypass the same lifecycle production uses.

Component stereotypes such as @RestController, @Service, and @Configuration make classes eligible for specific roles. A @Bean method is useful when construction needs code or the type comes from a third-party library. Neither mechanism should turn the context into a service locator fetched from business methods.

Configuration binding and precedence

Boot considers property sources in a defined order, with later sources able to override earlier ones. Command-line arguments outrank operating-system environment variables, and those outrank packaged configuration data. An override is useful only if operators and tests know which source won, so record the effective non-secret configuration needed to diagnose a deployment.

@ConfigurationProperties binds a related property namespace to a structured object. Relaxed binding maps catalog.page-size to an environment variable such as CATALOG_PAGE_SIZE, while validation annotations can reject missing or out-of-range values at startup. This is safer for a group of settings than scattering string keys and conversions across @Value fields.

Profiles select sets of configuration and beans; they do not create a security boundary. A secret committed to application-prod.properties remains a committed secret. Supply sensitive values through the deployment’s secret mechanism, make required values fail closed, and avoid plausible development defaults for production credentials.

The HTTP path

With the Spring MVC starter present, Boot configures the dispatcher servlet, message converters, validation integration, error handling infrastructure, and an embedded servlet container. A request reaches the server, passes filters, is mapped to a controller method, invokes application services, and has its return value converted into an HTTP response. Each step is a contract boundary, not hidden magic.

Returning a Java record can produce JSON because a compatible JSON mapper and message converter are on the classpath. That does not define authorization, transactionality, status semantics, or a stable public schema. The controller must still validate transport input, call a service with explicit domain rules, and return the statuses and fields promised by the API contract.

Examples

These three examples form one small catalog project generated by Spring Initializr with Maven, Java, Spring Boot 4.1.1, Spring Web MVC, Validation, and Actuator. They were compiled and executed with the generated Maven wrapper and local OpenJDK 21.0.12; the source uses APIs and language features valid for the target Java 25 LTS. The displayed output is from those runs.

Starting a JSON endpoint

The application class supplies the configuration root and executable entry point. The adjacent controller is discovered by component scanning, and Boot configures the server and JSON conversion because the Spring MVC starter is present.

CatalogApplication.java
package com.codewiki.catalog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
class ProductController {
    @GetMapping("/products/{sku}")
    Product find(@PathVariable String sku) {
        return new Product(sku, "Mechanical keyboard", 12900);
    }
}

record Product(String sku, String name, int priceInCents) {}
HTTP/1.1 200
Content-Type: application/json

{"sku":"KB-1","name":"Mechanical keyboard","priceInCents":12900}

The response proves routing and conversion for one happy path. It does not prove that the SKU exists, that the caller may see it, or that priceInCents is the intended public representation. Those rules belong in a service and the API contract rather than in assumptions attached to auto-configuration.

The server started on the default port because no override was supplied. A real deployment should set the port and proxy behavior explicitly where they differ, then test through the deployed ingress. An embedded server removes the external WAR deployment step; it does not remove the network boundary.

Binding validated settings

The next file registers a typed configuration object and prints its effective values after the context starts. @Validated applies Jakarta Validation constraints during binding, so invalid configuration stops startup before the runner or an HTTP handler can use it.

CatalogConfiguration.java
package com.codewiki.catalog;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;

@ConfigurationProperties("catalog")
@Validated
record CatalogSettings(
        @NotBlank String currency,
        @Min(1) @Max(100) int pageSize) {}

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CatalogSettings.class)
class CatalogConfiguration {
    @Bean
    ApplicationRunner showSettings(CatalogSettings settings) {
        return args -> System.out.printf(
                "catalog currency=%s pageSize=%d%n",
                settings.currency(), settings.pageSize());
    }
}
catalog currency=EUR pageSize=24

The run set CATALOG_CURRENCY=EUR and CATALOG_PAGE_SIZE=24, then selected a non-web application so it could exit after the runner. The environment variable names demonstrate relaxed binding. Packaged defaults of catalog.currency=USD and catalog.page-size=20 were overridden by the environment.

Printing selected non-secret settings can make precedence diagnosable. Never dump the entire environment or configuration-property endpoint into logs: it may contain passwords, tokens, internal hosts, and personal data. Treat redaction and endpoint access as part of the operational contract.

Testing the configured web boundary

This test starts the Boot context without a listening server and attaches MockMvc to the configured Spring MVC stack. It asserts status, compatible media type, and one field before printing the response. The Boot 4 import path for AutoConfigureMockMvc is intentionally shown.

CatalogApplicationTests.java
package com.codewiki.catalog;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class CatalogApplicationTests {
    @Autowired
    MockMvc mvc;

    @Test
    void returnsAProduct() throws Exception {
        var response = mvc.perform(get("/products/KB-1"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith("application/json"))
                .andExpect(jsonPath("$.sku").value("KB-1"))
                .andReturn().getResponse();

        System.out.println(response.getStatus() + " " + response.getContentAsString());
    }
}
200 {"sku":"KB-1","name":"Mechanical keyboard","priceInCents":12900}

MockMvc covers controller mapping, configured filters, argument resolution, and message conversion without a socket. It does not prove servlet-container behavior, TLS, proxy headers, or the packaged artifact. Add a random-port test or a deployed smoke test when those layers are part of the risk.

The full context makes this a useful wiring test but loads more than a focused web slice. Use a slice when its omitted collaborators are replaced deliberately and another test proves complete startup. Choose the smallest test that answers the question without pretending omitted layers were exercised.

Pitfalls

Scanning the wrong package tree

Fix: put the application class in a named root package above application components, or declare scan packages explicitly when the module layout requires it. Keep a context test that resolves a representative controller and service, and treat scan-boundary changes as behavioral changes.

Treating starters as harmless conveniences

Fix: add one capability for one requirement and inspect the dependency graph. After each starter, check the condition report, startup logs, exposed ports, and endpoints. Remove direct versions for artifacts managed by the selected Boot release unless a documented compatibility need justifies them.

Fighting auto-configuration blindly

Fix: identify the exact condition and documented replacement type first. Define the narrow application bean or property that triggers back-off, then rerun the context and condition report. Reserve exclusions for cases where the whole auto-configuration is intentionally inapplicable.

Accepting malformed configuration

Fix: group related settings under @ConfigurationProperties, use domain types such as Duration, apply validation constraints, and test valid, missing, and boundary values. Do not give secrets or dangerous production settings plausible fallbacks merely to make local startup green.

Exposing Actuator as an afterthought

Fix: expose only named endpoints, separate exposure from access control, and enforce the boundary with Spring Security, a management network, or both. Verify anonymous and operator access through the real proxy. Keep health details minimal unless the caller is authorized to see dependencies.

Confusing one test style with complete evidence

Fix: map risks to layers. Keep plain unit tests for business logic, a focused slice for transport behavior, a full context test for wiring, and a small random-port or deployed suite for the real network edge. Assert failures and stored side effects, not only a successful status.

Deep Condition evaluation and back-off

Condition evaluation and back-off

Auto-configuration is ordinary configuration selected through imports and guarded by conditions. Boot records its candidates in library metadata rather than discovering arbitrary configuration classes by scanning every dependency. This keeps the candidate set deliberate, although each candidate can still contain several conditional bean definitions.

Conditions answer questions such as whether a class exists, whether a property has a value, whether the application is servlet-based, and whether a bean is missing. Their evaluation depends on what has already been registered. Application configuration is intended to replace defaults, while relying on incidental ordering between two application beans is fragile.

The condition report is evidence, not a configuration API. A positive match explains why a candidate applied; a negative match explains why it did not. Read the full outcome because one auto-configuration can match at class level while an individual bean method backs away.

For a reusable library, custom Boot integration normally uses @AutoConfiguration, narrow @ConditionalOnClass checks, property binding, and @ConditionalOnMissingBean at replacement points. It is registered in that library’s auto-configuration imports metadata. Application-local wiring normally stays as @Configuration or explicit beans and does not need to impersonate a starter.

Avoid making one auto-configuration depend on a component scan of user packages. Library defaults should activate from explicit conditions and stable types, then allow applications to replace them. This keeps startup explainable and prevents a dependency from unexpectedly adopting application components outside its contract.

When two beans implement one interface, injection by type becomes ambiguous unless one candidate is qualified or primary, or the consumer asks for a collection. @ConditionalOnMissingBean can prevent a default from joining that set, but only for the types and search strategy declared by the condition. Inspect the actual bean names and types instead of assuming a same-named method replaced it.

Configuration and lifecycle boundaries

Property binding happens during context creation. Conversion can turn text into integers, enums, durations, data sizes, addresses, and structured collections before application beans use them. Validation then turns an invalid deployment contract into a startup error with the property path near the cause.

Precedence is useful for layered deployment, but too many sources make provenance hard to see. Keep packaged defaults safe and non-secret, use environment-specific configuration for deployment choices, and reserve command-line overrides for deliberate operations. Document how to inspect the effective value without revealing the secret itself.

Profiles are additive selectors and several can be active together. If two active profiles define the same property or bean, ordinary precedence and bean-resolution rules still apply. A profile name such as prod neither validates the environment nor prevents a developer from activating it locally.

Singleton beans are created for a context and often shared by concurrent requests. Store immutable configuration and thread-safe collaborators there, not current-user or request state. Request-scoped objects can model per-request state, but injecting them into a singleton requires a scope-aware proxy or another provider boundary that should remain visible in design and tests.

Startup work belongs in lifecycle hooks only when the application cannot serve correctly without it. Remote calls during bean construction slow every context test and can make deployments depend on a temporary service outage. Prefer local validation at startup and explicit readiness for dependencies that may recover after the process begins.

Shutdown is also a lifecycle boundary. The context can invoke destruction callbacks and stop the embedded server, but it cannot infer how long in-flight work may take or whether a consumed message can be replayed. Configure graceful shutdown and orchestration timeouts from the actual workload, then test termination under traffic.

Testing the configured application

Different Boot tests create different contexts, and context caching reuses a compatible context across tests. Changing properties, mock definitions, profiles, or imported configuration can create another cache key. A suite with many almost-identical contexts can therefore be slow even when every individual test appears small.

Test boundaryWhat it provesWhat remains outside
Plain JUnit testJava logic and collaborator contractSpring wiring and framework behavior
MVC sliceController mapping, binding, validation, conversionMost services and infrastructure
@SpringBootTest with MockMvcFull context plus mocked servlet request pathListening server, TLS, proxy, packaging
Random-port testEmbedded server and real HTTP client pathExternal ingress and production platform
Deployed smoke testSelected behavior through the release environmentExhaustive domain and failure behavior

@SpringBootTest does not start a server by default. Its mock web environment can be paired with @AutoConfigureMockMvc; RANDOM_PORT starts a real server on an available port. Select the latter only when socket and server behavior matter, because it increases cost without improving a pure service logic assertion.

A slice intentionally filters configuration. Replacing every missing collaborator with a mock may make the slice compile while concealing that production cannot construct the graph. Keep at least one test that starts the same primary configuration and profiles as the deployable application.

Context success is necessary but weak. It does not prove that an endpoint enforces object ownership, that a transaction rolls back, or that retries do not duplicate work. Pair wiring evidence with scenario assertions at the layer where each side effect and failure becomes observable.

Operations without accidental disclosure

Actuator contributes endpoints only when its dependency and auto-configuration apply. Endpoint access and technology exposure are separate decisions: an endpoint can exist without being exposed over HTTP, and an exposed endpoint still needs an access policy. In Spring Boot 4.1.1, only health is exposed over HTTP and JMX by default.

Health means the application can report a status, not that every dependency should appear in anonymous output. By default, DOWN and OUT_OF_SERVICE map to HTTP 503 while UP and unmapped statuses map to 200. Clients should use the documented group and status rather than parse component text.

Liveness should answer whether restarting this process can help. Including a shared database in liveness can restart every replica during a database outage and amplify the incident. Readiness may include dependencies required to serve, but its policy must match whether load should be removed and how quickly the dependency can recover.

The conditions, configprops, env, beans, mappings, and logging endpoints are powerful diagnostic surfaces. Redaction reduces risk but does not replace authorization and network controls. Audit changes through writable endpoints and test that a public route cannot reach the management surface through proxy rewriting.

Metrics need stable names, bounded tag cardinality, and ownership. Putting user IDs, raw paths, order IDs, or exception messages into tags can create unbounded series and leak data. Auto-configured metrics provide a starting point; service-level indicators still require explicit definitions tied to user outcomes.

Further reading

checkpoint

5 questions · 1 predict-the-output · 1 spot-the-bug

next up Testing soon Backend security soon Database design soon Kotlin backend development
Copy as Markdown Interview bank Edit on GitHub Report an error Was this clear?