Spring Boot creates stand-alone Spring applications by combining the Spring container with conditional configuration, curated dependencies, an embedded server, and operational support.
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.
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.
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.
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=24The 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.
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.
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 boundary | What it proves | What remains outside |
|---|---|---|
| Plain JUnit test | Java logic and collaborator contract | Spring wiring and framework behavior |
| MVC slice | Controller mapping, binding, validation, conversion | Most services and infrastructure |
@SpringBootTest with MockMvc | Full context plus mocked servlet request path | Listening server, TLS, proxy, packaging |
| Random-port test | Embedded server and real HTTP client path | External ingress and production platform |
| Deployed smoke test | Selected behavior through the release environment | Exhaustive 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
5 questions · 1 predict-the-output · 1 spot-the-bug