Spring Boot Reference programmer’s guide · 262 annotations indexed · Spring Boot 4.1 · Java 17–26

Written for someone who already knows what a transaction, a proxy and an isolation level are, and wants the Spring-specific facts: the module split and the starter renames, exactly how auto-configuration decides, the fifteen-deep property precedence, what the proxy silently declines to do, which defaults are wrong for production, and what Boot 4 broke. Pages 1–3 are the guide; the rest index every annotation you are likely to meet, grouped by what it does. Type in the filter box — or press / — to narrow the index; hover any annotation for its full description and where it comes from.

Comes from: Spring Framework Spring Boot Jakarta EE / Bean Validation Spring Data, Security & portfolio deprecated or removed
Sources: spring.io/projects/spring-boot and docs.spring.io (Spring Boot 4.1.1 reference, Spring Framework 7, Spring Data JPA), the Spring Boot 4.0 release notes and migration guide, endoflife.date/spring-boot, the JRebel Spring annotations cheat sheet, dev.to/burakboduroglu, codingshuttle.com, github.com/RohanVishwakarma001/Spring-Boot-Complete-Cheat-sheet, and Mark Heckler, Spring Boot: Up and Running (O’Reilly). Hover any annotation for its description.

Spring Boot Programmer's Guide

The framework as the 4.x line implements it — what it does, how it decides, and where it bites

What Spring Boot Is

Not a framework — a packaging of one. Spring Boot removes two jobs from a Spring application: resolving a mutually compatible dependency set, and writing the configuration that wires it together. Four mechanisms do all of it.

MechanismWhat it removes
StartersA curated dependency (spring-boot-starter-*) pulls in a technology and everything it needs. A parent BOM pins every version, so you name artifacts without versions.
Auto-configurationOn startup Boot inspects the classpath, the existing beans and the properties, then defines the beans you did not. Put Hibernate on the path and you get a DataSource, an EntityManagerFactory and a transaction manager.
Embedded serverTomcat (or Jetty, or Netty) is a library inside your jar. java -jar app.jar is the deployment. There is no container to install and no WAR to drop.
ActuatorHealth, metrics, environment, mappings, loggers and thread dumps over HTTP or JMX, without writing any of it.

A fifth, Spring Initializr, is not part of the runtime — it is the generator that writes the first commit for you.

The rule that makes it work

Every auto-configuration backs off the moment you define the bean yourself. @ConditionalOnMissingBean is on almost all of it. That is why Boot feels like magic and behaves like a library: your bean always wins.

Versions & Support

Two releases a year, in May/June and November. Each gets 13 months of free OSS support from GA and a further year commercially; 2.7 and 3.5 are the long-lived exceptions.

BranchGAOSS endsCommercialJava
4.1Jun 2026Jul 2027Jul 202817–26
4.0Nov 2025Dec 2026Dec 202717–25
3.5May 2025Jun 2026Jun 203217–25
3.4Nov 2024Dec 2025Dec 202617–24
3.0Nov 2022Dec 2023Dec 202417–21
2.7May 2022Jun 2023Jun 20298–21

What 4.1 requires

ThingVersion
Java17 minimum, 26 maximum
Spring Framework7.0.9 or later
Maven3.6.3+
Gradle8.14+ (8.x line) or 9.x
Servlet containersTomcat 11.0.x, Jetty 12.1.x — Servlet 6.1; any Servlet 6.1+ container for a WAR
GraalVMCommunity 25, Native Build Tools 1.1.8

Boot 3.x is the Jakarta EE line (javax.*jakarta.*); Boot 2.x is not. Boot 4 adds Spring Framework 7, Spring Security 7 and Jackson 3 on top.

Starting a Project

From the command line

curl https://start.spring.io/starter.zip \ -d type=maven-project -d language=java \ -d bootVersion=4.1.1 -d javaVersion=21 \ -d groupId=cloud.geocam -d artifactId=demo \ -d dependencies=webmvc,data-jpa,postgresql,actuator \ -o demo.zipcurl start.spring.ioplain-text help, all optionscurl start.spring.io/dependenciesevery dependency id

Maven — the parent does the version management

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.1.1</version>
</parent>
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
  </dependency>
</dependencies>

Can’t inherit from the parent? Import spring-boot-dependencies as a BOM with <scope>import</scope> in dependencyManagement instead — you keep version management, you lose the plugin defaults.

Gradle

plugins {
  id 'java'
  id 'org.springframework.boot' version '4.1.1'
  id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-webmvc'
  developmentOnly 'org.springframework.boot:spring-boot-devtools'
}

The layout Boot expects

cloud.geocam.demo
├── DemoApplication.java   ← @SpringBootApplication
├── web/       controllers, DTOs, advice
├── service/   business logic
├── repository/ Spring Data interfaces
└── domain/    entities

src/main/resources
├── application.yaml
├── application-dev.yaml
├── static/    css, js, images  → served at /
└── templates/ Thymeleaf, Mustache…

Component scanning starts at the package of the class carrying @SpringBootApplication and goes down. Put that class at the root of your package tree, or nothing is found.

The Application Class

package cloud.geocam.demo;

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

@SpringBootApplication is three annotations

Meta-annotationEffect
@SpringBootConfigurationA @Configuration class, and the marker tests use to find the context root
@EnableAutoConfigurationTurns on classpath-driven configuration
@ComponentScanScans this package and below for stereotypes

Attributes worth knowing

exclude = {DataSourceAutoConfiguration.class}drop one auto-configurationscanBasePackages = "cloud.geocam"scan somewhere elseproxyBeanMethods = falselite mode; faster, no inter-bean call proxying

Taking control of the launch

var app = new SpringApplication(DemoApplication.class);
app.setBannerMode(Banner.Mode.OFF);
app.setWebApplicationType(WebApplicationType.NONE);
app.setDefaultProperties(Map.of("server.port", "9090"));
app.run(args);

// or the fluent builder
new SpringApplicationBuilder(DemoApplication.class)
    .profiles("dev").web(WebApplicationType.SERVLET).run(args);

Running code at startup

CommandLineRunnerrun(String... args) — raw argvApplicationRunnerrun(ApplicationArguments) — parsed options@EventListener(ApplicationReadyEvent.class)after the server is accepting traffic@PostConstructper-bean, before the context is up — not for slow work

Order several runners with @Order. An exception thrown from a runner aborts startup.

Starters

A starter is a POM with no code: it names a technology and drags in a consistent set of dependencies. Boot 4 split the old megajars into one module per technology, so the names changed — and the “test” half of every starter is now its own artifact.

StarterGets you
spring-boot-starterCore: auto-configuration, logging, YAML. Everything else builds on it.
-webmvcSpring MVC + embedded Tomcat. Replaces -web, now deprecated.
-webfluxReactive stack: WebFlux + Reactor Netty
-data-jpaSpring Data JPA with Hibernate, plus HikariCP
-jdbcJdbcClient/JdbcTemplate and HikariCP, no ORM
-data-jdbc / -data-r2dbcSpring Data JDBC; R2DBC for reactive SQL
-data-mongodb, -data-redis, -data-elasticsearch, -data-cassandraNoSQL, each with a -reactive twin where one exists
-securitySpring Security: filter chain, form login, HTTP Basic
-security-oauth2-client / -resource-server / -authorization-serverOAuth2 and OIDC. The old names without security- are deprecated.
-validationBean Validation via Hibernate Validator
-actuatorProduction endpoints
-restclient / -webclientBlocking (RestClient) or reactive (WebClient) HTTP clients, plus HTTP Service Clients
-flyway / -liquibaseMigrations. New in Boot 4 — the bare third-party dependency is no longer enough.
-cache, -quartz, -mail, -batch, -integration, -graphql, -kafka, -amqp, -websocket, -thymeleaf, -freemarker, -mustache, -hateoas, -jooq, -pulsar, -rsocket, -grpc-client/-serverOne each, doing what the name says
-opentelemetry, -micrometer-metrics, -zipkinMetrics and tracing export

Swapping the container

<dependency>
  <artifactId>spring-boot-starter-webmvc</artifactId>
  <exclusions><exclusion>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
  </exclusion></exclusions>
</dependency>
<dependency>
  <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

Test starters

spring-boot-starter-test (JUnit Jupiter, AssertJ, Hamcrest, Mockito, JSONassert, JsonPath) still exists, but Boot 4 wants you to list the per-technology ones instead: -webmvc-test, -data-jpa-test, -security-test, -restclient-test. spring-boot-starter-classic and -test-classic restore the old all-in-one behaviour while you migrate.

Beans, IoC & Injection

Two ways to get a definition into the BeanFactory: scanning, or a factory method. Everything else — qualifiers, scopes, proxies — is refinement on top.

Declaring one

@Service                             // stereotype: scanned
class OrderService { }

@Configuration                       // factory method: explicit
class AppConfig {
  @Bean
  RestClient restClient(RestClient.Builder b) {
    return b.baseUrl("https://api.example.com").build();
  }
}

Constructor injection, and nothing else

@Service
public class OrderService {
  private final OrderRepository repo;      // final: immutable
  private final PaymentClient payments;

  OrderService(OrderRepository repo, PaymentClient payments) {
    this.repo = repo; this.payments = payments;
  }
}

A single constructor is autowired implicitly — no @Autowired. Fields stay final, no half-constructed state is reachable, and the test constructs it with new. Field injection needs reflection to substitute, and removes the pressure that makes a nine-dependency constructor look as wrong as it is. It also cannot express a circular dependency as a compile-time impossibility, which is why circular-reference failures surface at startup with constructor injection and at first use without it.

Stereotypes

AnnotationMeans
@ComponentGeneric bean; the other three are specialisations of it
@ServiceBusiness logic. Semantic only — no extra behaviour
@RepositoryData access. Does add behaviour: translates vendor exceptions into Spring’s DataAccessException hierarchy
@Controller / @RestControllerWeb endpoint; @RestController = @Controller + @ResponseBody

Choosing among several candidates

@Primarythis one wins by default@Qualifier("name")pick by bean name at the injection point@Fallbackuse only when nothing else matches (Framework 6.2+)List<Validator>inject them all, in @Order orderMap<String,Validator>all of them, keyed by bean nameObjectProvider<Foo>optional / lazy / plural lookup

Scopes

ScopeOne instance per
singletoncontainer (the default)
prototypeinjection point — container does not manage its destruction
request / session / applicationweb request / HTTP session / ServletContext

Injecting a prototype into a singleton gives you one instance forever. Use ObjectProvider, @Lookup, or a scoped proxy.

Lifecycle

@PostConstructafter injection, before use@PreDestroyon graceful shutdown@DependsOn("flyway")force another bean to initialise first@Lazybuild on first use, not at startup@Order / Orderedposition in an injected collection

Auto-configuration

@EnableAutoConfiguration reads every META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file on the classpath — one line per class — and evaluates each class’s conditions. Classes whose conditions match contribute beans; the rest are skipped and never loaded.

The conditions

ConditionMatches when
@ConditionalOnClassa class is on the classpath (the usual first gate)
@ConditionalOnMissingClassit is not
@ConditionalOnBeana bean of that type is already defined
@ConditionalOnMissingBeanit is not — the back-off that lets your bean win
@ConditionalOnPropertyprefix+name has havingValue; matchIfMissing decides the default
@ConditionalOnBooleanPropertythe same, for a plain boolean
@ConditionalOnResourcea resource exists, e.g. classpath:banner.txt
@ConditionalOnWebApplicationservlet or reactive web context (type=SERVLET|REACTIVE|ANY)
@ConditionalOnNotWebApplicationplain context
@ConditionalOnWarDeployment / Notdeployed as a WAR / running embedded
@ConditionalOnExpressiona SpEL expression is true

Ordering

@AutoConfiguration(before=, after=, beforeName=, afterName=), or the standalone @AutoConfigureBefore / @AutoConfigureAfter / @AutoConfigureOrder. Ordering affects bean definition order only; creation order still follows dependencies.

Writing your own

@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass(AcmeClient.class)
@EnableConfigurationProperties(AcmeProperties.class)
public class AcmeAutoConfiguration {
  @Bean @ConditionalOnMissingBean
  AcmeClient acmeClient(AcmeProperties p) {
    return new AcmeClient(p.url(), p.token());
  }
}

Register the class in src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Use $ for nested classes. Never put auto-configuration classes in a package that gets component-scanned.

Seeing what it decided

java -jar app.jar --debugcondition evaluation report to the consoleGET /actuator/conditionsthe same report as JSON, liveGET /actuator/beansevery bean actually definedspring.autoconfigure.exclude=…turn one off by property

Configuration & Properties

Everything configurable is a property, and properties come from many places. Later sources in this list win.

PropertySource precedence — low to high

#Source
1SpringApplication.setDefaultProperties
2@PropertySource on a @Configuration class
3Config data — application.properties / .yaml and their profile variants
4random.*
5OS environment variables
6Java system properties (-D)
7–9JNDI java:comp/env, ServletContext and ServletConfig init params
10SPRING_APPLICATION_JSON
11Command-line arguments (--server.port=9090)
12–14Test-only: @SpringBootTest(properties=), @DynamicPropertySource, @TestPropertySource
15DevTools global settings in $HOME/.config/spring-boot

Where config files are found

Classpath root, classpath /config, the current directory, ./config/, and immediate children of ./config/ — in that order, each overriding the last. Inside a location, .properties beats .yaml, and a profile-specific file always beats the plain one.

Binding: @ConfigurationProperties

@ConfigurationProperties(prefix = "acme")
public record AcmeProperties(
    URI url, String token, Duration timeout,
    @DefaultValue("3") int retries) { }

// switch it on, once:
@ConfigurationPropertiesScan          // on the app class
// or @EnableConfigurationProperties(AcmeProperties.class)

Records get constructor binding automatically. Prefer this to scattering @Value: it is typed, validatable (@Validated + constraints), relocatable, and shows up in /actuator/configprops and in your IDE’s completion if you add spring-boot-configuration-processor.

Relaxed binding

One property, many spellings. acme.api-key in a file, acme.apiKey in code, ACME_APIKEY in the environment, acme.api_key in a system property — all bind to the same field. Kebab-case in files is the canonical form; use it in ${…} placeholders too.

Values, placeholders and imports

@Value("${acme.token}")single value; fails fast if missing@Value("${acme.token:none}")with a default${app.name} is ${user:Unknown}placeholders nest inside valuesspring.config.import=optional:file:./dev.propertiespull in another file; imported values winspring.config.import=configtree:/etc/config/Kubernetes secrets as a directory of filesspring.config.location=classpath:/cfg/;classpath:/ext/replace the search path; ; separates groups

Multi-document files

server.port=8080
#---
spring.config.activate.on-profile=prod
spring.config.activate.on-cloud-platform=kubernetes
server.port=80

In YAML the separator is ---; in .properties it is a #--- comment line. Multi-document files do not work through @PropertySource.

Profiles

A profile is a named slice of configuration and beans. Nothing about it is environment-specific until you say so.

spring.profiles.active=dev,localturn them on--spring.profiles.active=prod…from the command lineSPRING_PROFILES_ACTIVE=prod…from the environmentspring.profiles.default=devused when none are activespring.profiles.include=commonalways add these as wellspring.profiles.group.prod=metrics,cloudone name activates several

Two things profiles switch

// beans
@Component @Profile("!prod")
class InMemoryPaymentGateway implements PaymentGateway { }

# files, loaded on top of application.yaml
application-dev.yaml
application-prod.yaml

Expressions work too: @Profile("prod | staging"), @Profile("!test"). With several active profiles it is last-wins, so active=prod,live lets application-live override application-prod.

The trap: spring.profiles.active cannot be set inside a profile-specific file. Set it outside the application entirely — environment, argument, or the plain application.yaml.

REST Controllers

@RestController
@RequestMapping("/api/orders")
class OrderController {
  private final OrderService service;
  OrderController(OrderService service) { this.service = service; }

  @GetMapping                    // GET /api/orders?page=0&size=20
  Page<OrderDto> list(Pageable pageable) { … }

  @GetMapping("/{id}")           // GET /api/orders/42
  OrderDto one(@PathVariable long id) { … }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  OrderDto create(@Valid @RequestBody NewOrder body) { … }

  @PutMapping("/{id}")
  ResponseEntity<OrderDto> replace(@PathVariable long id,
                                   @Valid @RequestBody OrderDto body) {
    return ResponseEntity.ok(service.replace(id, body));
  }

  @DeleteMapping("/{id}")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  void delete(@PathVariable long id) { … }
}

Where an argument comes from

AnnotationReads
@PathVariablea {placeholder} in the URL template
@RequestParamquery string or form field; required, defaultValue
@RequestBodythe body, deserialised by Jackson
@RequestHeader / @CookieValueone header / one cookie
@RequestPartone part of a multipart upload
@ModelAttributea whole object bound from request params
@MatrixVariable;key=value segments in the path
no annotationresolved by type: Pageable, Principal, HttpServletRequest, UriComponentsBuilder

Shaping the response

return dto;200 with a JSON body@ResponseStatus(HttpStatus.CREATED)fixed status for the methodResponseEntity.created(uri).body(dto)status + headers, decided at runtimeResponseEntity.noContent().build()204ResponseEntity.notFound().build()404Optional<T> / null200 with an empty body — usually not what you want

Narrowing a mapping

@GetMapping(produces = "application/json")Accept must match@PostMapping(consumes = "application/json")Content-Type must match@GetMapping(params = "type=full")only with that query param@GetMapping(headers = "X-Api-Key")only when the header is present

API versioning — new in Boot 4

spring.mvc.apiversion.use.header=X-API-Versionwhere the version is read from@GetMapping(version = "1.1")this handler serves 1.1 and up

Also spring.mvc.apiversion.use.query-param, .path-segment or .media-type-parameter, plus ApiVersionResolver, ApiVersionParser and ApiVersionDeprecationHandler beans. Same properties under spring.webflux.*.

CORS

@CrossOrigin(origins = "https://app.example.com")per controller or methodWebMvcConfigurer#addCorsMappingsglobally

With Spring Security on the path, CORS must also be enabled in the filter chain (http.cors(withDefaults())) or the preflight is rejected before it reaches MVC.

Validation & Error Handling

Constrain the DTO, then ask for it to be checked

public record NewOrder(
    @NotBlank @Size(max = 80) String customer,
    @Email String email,
    @NotNull @Positive BigDecimal amount,
    @Future LocalDate deliverBy,
    @Valid @NotEmpty List<Line> lines) { }

@PostMapping
OrderDto create(@Valid @RequestBody NewOrder body) { … }

Needs spring-boot-starter-validation. @Valid on a nested field cascades; without it, nested objects are not checked. On a bean’s own methods use @Validated at class level to get method-parameter validation.

Turning failures into one JSON shape

@RestControllerAdvice
class ApiExceptionHandler {

  @ExceptionHandler(MethodArgumentNotValidException.class)
  ProblemDetail invalid(MethodArgumentNotValidException ex) {
    var pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
    pd.setTitle("Validation failed");
    pd.setProperty("errors", ex.getBindingResult().getFieldErrors()
        .stream().collect(toMap(FieldError::getField,
                                FieldError::getDefaultMessage)));
    return pd;
  }

  @ExceptionHandler(OrderNotFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  ProblemDetail missing(OrderNotFoundException ex) {
    return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,
                                            ex.getMessage());
  }
}

ProblemDetail is RFC 9457 — type, title, status, detail, instance, plus your own properties. Extend ResponseEntityExceptionHandler to get Spring’s own exceptions in the same shape, or set spring.mvc.problemdetails.enabled=true to have Boot do it for you.

The default error page

server.error.include-message=alwaysoff by default — that is why it says nothingserver.error.include-binding-errors=alwaysfield-level detailserver.error.include-stacktrace=on-param?trace=trueserver.error.whitelabel.enabled=falsedrop the default HTML page

Never turn stack traces on in production. /error is a real mapping; you can replace it with your own @Controller or an ErrorAttributes bean.

Spring Data JPA

The entity

@Entity @Table(name = "orders")
public class Order {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable = false, length = 80)
  private String customer;

  @Enumerated(EnumType.STRING)          // never ORDINAL
  private Status status;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "customer_id")
  private Customer owner;

  @OneToMany(mappedBy = "order", cascade = ALL, orphanRemoval = true)
  private List<Line> lines = new ArrayList<>();

  @Version private long version;         // optimistic locking
  @CreatedDate private Instant createdAt;
}

The repository

public interface OrderRepository
        extends JpaRepository<Order, Long> {

  List<Order> findByCustomerIgnoreCase(String customer);
  Page<Order> findByStatusOrderByCreatedAt(Status s, Pageable p);
  Optional<Order> findFirstByCustomerAndStatus(String c, Status s);
  boolean existsByReference(String reference);
  long countByStatus(Status s);

  @Query("select o from Order o join fetch o.lines where o.id = :id")
  Optional<Order> findWithLines(@Param("id") Long id);

  @Modifying @Transactional
  @Query("update Order o set o.status = :s where o.id = :id")
  int markStatus(@Param("id") Long id, @Param("s") Status s);
}

The hierarchy is RepositoryCrudRepositoryPagingAndSortingRepositoryJpaRepository. No implementation is written; Spring Data builds a proxy from the method names at startup, and a name it cannot parse is a startup failure, not a runtime one.

Derived query keywords

KeywordSQL
And / Or / Notand / or / <>
Is, Equals, or nothing=
LessThan(Equal), GreaterThan(Equal)< <= > >=
Between, Before, Afterbetween, <, >
(Is)Null, (Is)NotNullis null, is not null
Like, NotLikelike — you supply the %
StartingWith, EndingWith, Containinglike — Spring supplies the %
In, NotInin — takes a Collection, array or varargs
True, False= true, = false
IgnoreCaseupper(x) = upper(?)
Distinct, OrderBy…Asc|Descselect distinct, order by
findFirst5, findTop10limit the result

Paging and sorting

PageRequest.of(0, 20, Sort.by("createdAt").descending())build one by handPage<T>content + total count (an extra query)Slice<T>content + “is there more” — no count query?page=0&size=20&sort=createdAt,descbound straight into a Pageable parameter

Schema

PropertyNote
spring.jpa.hibernate.ddl-autonone | validate | update | create | create-drop. Boot defaults to create-drop for an embedded DB, none otherwise. Use validate in production and let Flyway own the schema.
spring.jpa.show-sqldumps SQL to stdout; prefer logging.level.org.hibernate.SQL=DEBUG
spring.jpa.open-in-viewdefaults to true and logs a warning. Set it false and fix the lazy-loading it was hiding.
spring.jpa.properties.hibernate.*passthrough for raw Hibernate settings

N+1, and the pagination trap under it

Cure the plain case with join fetch, @EntityGraph(attributePaths = "lines"), or a projection interface that never touches the association. But join fetch on a collection plus a Pageable makes Hibernate log HHH90003004 and paginate in memory after loading the whole result set — correct answer, unbounded heap. Split it: page the ids, then fetch the graph for that id set. Count queries to diagnose; reading the code will not show you this.

Transactions

@Service
class OrderService {
  @Transactional
  public Order place(NewOrder cmd) {
    var order = repo.save(Order.from(cmd));
    inventory.reserve(order);      // same transaction
    return order;                  // commit here, or roll back
  }

  @Transactional(readOnly = true)   // no dirty checking or flush
  public List<Order> recent() { return repo.findTop20ByIdDesc(); }
}

Propagation — what happens if one is already running

ValueBehaviour
REQUIREDjoin it, or start one. The default.
REQUIRES_NEWsuspend the outer, run in a fresh one. For audit rows that must survive a rollback.
NESTEDsavepoint inside the outer transaction
SUPPORTSjoin if there is one, otherwise run without
NOT_SUPPORTEDsuspend and run without
MANDATORY / NEVERthrow if there is not one / if there is

Isolation

The four ANSI levels plus DEFAULT, which delegates to the driver — and DEFAULT is what you almost always want, because the level you name here is passed straight through and PostgreSQL’s REPEATABLE_READ is snapshot isolation while MySQL’s is not. Setting isolation on a @Transactional that joins an existing transaction throws rather than silently downgrading.

Rollback rules

By default Spring rolls back on RuntimeException and Error, and commits on a checked exception. Say so if you mean otherwise:

@Transactional(rollbackFor = IOException.class)roll back on a checked one too@Transactional(noRollbackFor = NotFound.class)swallow this one@Transactional(timeout = 5)seconds

Two traps that cost an afternoon each

  • Self-invocation. this.otherMethod() does not go through the proxy, so its @Transactional, @Async or @Cacheable is ignored. Move the method to another bean, or inject the bean into itself.
  • Visibility. Proxy-based @Transactional only works on public methods of a Spring bean. On a private, protected or package-private method it silently does nothing.

HTTP Clients

Three shapes, one auto-configured builder each. RestTemplate is legacy — new blocking code uses RestClient.

RestClient — blocking, fluent

@Bean
RestClient orders(RestClient.Builder builder) {
  return builder.baseUrl("https://api.example.com")
                .defaultHeader("X-Api-Key", key)
                .build();
}

Pet pet = restClient.get()
    .uri("/pets/{id}", id)
    .accept(APPLICATION_JSON)
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError,
        (req, res) -> { throw new NotFound(res.getStatusCode()); })
    .body(Pet.class);

ResponseEntity<Void> created = restClient.post()
    .uri("/pets").contentType(APPLICATION_JSON).body(pet)
    .retrieve().toBodilessEntity();

HTTP Service Clients — declarative

@HttpExchange("/pets")
interface PetClient {
  @GetExchange("/{id}")  Pet byId(@PathVariable long id);
  @PostExchange          Pet create(@RequestBody Pet pet);
  @DeleteExchange("/{id}") void delete(@PathVariable long id);
}

@Configuration
@ImportHttpServices(group = "pets", types = PetClient.class)
class ClientConfig { }

Boot 4 auto-configures the proxy factory, so the interface is injectable as a bean; group configuration goes through a RestClientHttpServiceGroupConfigurer. Method parameters take the familiar @PathVariable, @RequestParam, @RequestHeader, @RequestBody, @CookieValue, @RequestPart.

WebClient — reactive

Mono<Pet> pet = webClient.get().uri("/pets/{id}", id)
    .retrieve().bodyToMono(Pet.class);

Settings that matter

spring.http.client.connect-timeout=2sblocking clientsspring.http.client.read-timeout=10s…and don't leave this unsetspring.http.client.factory=jdk|http-components|jetty|reactor|simplewhich transportspring.http.reactiveclient.*the WebClient equivalents

Security

Add spring-boot-starter-security and every endpoint is locked immediately, with a generated password on the console. That default is a prompt to configure, not a configuration.

The filter chain (Spring Security 7 — lambda DSL only)

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig {

  @Bean
  SecurityFilterChain api(HttpSecurity http) throws Exception {
    return http
      .securityMatcher("/api/**")
      .csrf(csrf -> csrf.disable())        // stateless API only
      .cors(Customizer.withDefaults())
      .authorizeHttpRequests(a -> a
          .requestMatchers("/api/public/**").permitAll()
          .requestMatchers(GET, "/api/orders/**").hasRole("USER")
          .requestMatchers("/api/admin/**").hasRole("ADMIN")
          .anyRequest().authenticated())
      .sessionManagement(s -> s
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
      .build();
  }

  @Bean PasswordEncoder encoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }
}

Several chains can coexist; each needs a securityMatcher and they are tried in @Order. The old WebSecurityConfigurerAdapter is long gone, and Security 7 removed the .and() chaining style — lambdas everywhere.

Bearer tokens

spring.security.oauth2.resourceserver.jwt.issuer-uri=…discovery: keys and validation configured for youspring.security.oauth2.resourceserver.jwt.jwk-set-uri=…keys onlyAuthorization: Bearer <token>what the client sends@AuthenticationPrincipal Jwt jwtthe token, in a handler method

Writing your own OncePerRequestFilter to parse JWTs is the tutorial answer; the resource-server support is the maintained one. If you do write a filter, register it with addFilterBefore(…, UsernamePasswordAuthenticationFilter.class) and populate the SecurityContextHolder.

Method security

@PreAuthorize("hasRole('ADMIN')")checked before the call@PreAuthorize("#id == authentication.name")argument-aware, via SpEL@PostAuthorize("returnObject.owner == authentication.name")checked on the return value@PreFilter / @PostFilterstrip elements from a collection@Secured("ROLE_ADMIN")older, no SpEL

Three details that cause most of the confusion

  • hasRole("ADMIN") tests for the authority ROLE_ADMIN; hasAuthority adds no prefix. Mixing them is why a rule that looks right denies everything.
  • Rules in authorizeHttpRequests are evaluated in declaration order, first match wins — anyRequest() placed early makes everything after it dead code, and Security refuses to start if it can prove a matcher is unreachable.
  • CSRF is on by default and belongs anywhere the browser supplies the credential automatically (cookie or session). Disabling it is correct only when the credential is a header the caller must set deliberately.

Caching

@EnableCaching               // once, on a @Configuration class
@Service
class CatalogService {

  @Cacheable(value = "products", key = "#id",
             unless = "#result == null")
  Product byId(long id) { … }        // slow call, memoised

  @CachePut(value = "products", key = "#p.id")
  Product save(Product p) { … }   // always runs, refreshes it

  @CacheEvict(value = "products", key = "#id")
  void delete(long id) { … }

  @CacheEvict(value = "products", allEntries = true)
  void reindex() { … }
}
AttributeMeaning
keySpEL over the arguments; default is all arguments combined
conditionevaluated before the call — cannot see the result
unlessevaluated after#result is in scope
sync = trueone caller computes, the rest wait
beforeInvocationon @CacheEvict: evict even if the method throws
@CacheConfigclass-level defaults for cache names and key generator
@Cachingseveral cache annotations of the same kind on one method

Providers

Backing storeWhen
ConcurrentHashMapthe fallback if nothing else is present. No eviction, no size limit — a memory leak with a nice name.
Caffeinesingle instance, real eviction. spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=5m
Redisshared across instances. spring.cache.type=redis, spring.cache.redis.time-to-live=10m
Hazelcast, JCache, Couchbaseauto-detected when on the classpath

Cache proxies are subject to the same self-invocation rule as transactions. And cache what is expensive and stable — caching a method that is already a primary-key lookup buys nothing and costs correctness.

Async & Scheduling

@Async

@Configuration @EnableAsync
class AsyncConfig {
  @Bean("mailExecutor")
  ThreadPoolTaskExecutor mailExecutor() {
    var ex = new ThreadPoolTaskExecutor();
    ex.setCorePoolSize(4); ex.setMaxPoolSize(16);
    ex.setQueueCapacity(200); ex.setThreadNamePrefix("mail-");
    return ex;
  }
}

@Async("mailExecutor")
CompletableFuture<Receipt> send(Order o) { … }

Return void and you lose every exception unless you register an AsyncUncaughtExceptionHandler. Return CompletableFuture and the caller can join or compose. Boot’s default executor is configured under spring.task.execution.*; on Java 21+, spring.threads.virtual.enabled=true puts the whole application on virtual threads.

@Scheduled

@Configuration @EnableScheduling
class SchedulingConfig { }

@Scheduled(fixedDelay = 30_000)  // 30s after the last one ended
void poll() { … }

@Scheduled(fixedRate = 1000, initialDelay = 5000)
void tick() { … }              // every 1s regardless of duration

@Scheduled(cron = "0 15 3 * * MON-FRI", zone = "Europe/London")
void nightly() { … }                     // 03:15 on weekdays

@Scheduled(cron = "${reports.cron:-}")   // "-" disables it
void report() { … }

Spring’s cron expression is six fields, seconds first — not the five-field Unix form, so a copied crontab line is off by a factor of sixty. L and # are supported; ? is accepted and means the same as *. Macros: @hourly, @daily, @weekly, @monthly, @yearly.

Two things that surprise people. The scheduler pool is one thread by default, so a slow job delays every other job in the application — set spring.task.scheduling.pool.size. And @Scheduled fires on every instance in a cluster; if the job is not idempotent you need a distributed lock (ShedLock, or a row you SELECT … FOR UPDATE SKIP LOCKED).

Messaging

Three brokers, one shape: a listener annotation on a method, a template for sending, and auto-configuration that reads a handful of properties.

Kafka

@KafkaListener(topics = "orders", groupId = "billing")
void onOrder(Order order, Acknowledgment ack) { … }

kafkaTemplate.send("orders", order.id().toString(), order);
spring.kafka.bootstrap-servers=localhost:9092the clusterspring.kafka.consumer.group-id=billingconsumer groupspring.kafka.consumer.auto-offset-reset=earliestwhere a new group startsspring.kafka.listener.ack-mode=manualcommit when you say so

RabbitMQ (AMQP)

@RabbitListener(queues = "orders")
void onOrder(Order order) { … }

rabbitTemplate.convertAndSend("orders.ex", "order.created", order);
spring.rabbitmq.host / .port / .username / .passwordconnectionspring.rabbitmq.listener.simple.retry.enabled=trueredelivery with backoff

JMS

@JmsListener(destination = "orders")
void onOrder(@Payload Order order,
             @Header("JMSCorrelationID") String cid) { … }

Common ground

@SendTo("replies")route the return value to another destination@Payload / @Header / @Headersbind parts of the message@MessageMappingSTOMP over WebSocket, and RSocket

Serialisation is the recurring bug: agree a format (JSON via JsonMessageConverter, or Avro/Protobuf with a registry) rather than letting Java serialisation happen. And make every consumer idempotent — all three brokers are at-least-once.

Logging

SLF4J is the API, Logback the default implementation, and every starter pulls in spring-boot-starter-logging. You configure it with properties, not XML, until you need something Logback-specific.

private static final Logger log =
    LoggerFactory.getLogger(OrderService.class);

log.info("Placed order {} for {}", order.id(), name);  // parameterised
log.debug("Payload: {}", () -> expensive());  // lazily evaluated
log.error("Payment failed for {}", order.id(), ex);  // throwable last

The {} form is not cosmetic: with + the concatenation and every toString() run before the level check. The Supplier overload defers even the argument evaluation. An exception passed as the last argument is treated as a throwable and gets a stack trace, not a {} substitution — which is why a message with a trailing {} and an exception loses the trace.

Levels and groups

logging.level.root=INFOTRACE < DEBUG < INFO < WARN < ERROR < OFFlogging.level.cloud.geocam=DEBUGper package or classlogging.level.org.hibernate.SQL=DEBUGsee the SQLlogging.level.org.hibernate.orm.jdbc.bind=TRACE…and its parameterslogging.level.web=DEBUGbuilt-in group: MVC, codecs, HTTP clientslogging.level.sql=DEBUGbuilt-in group: JDBC, JPA, R2DBClogging.group.acme=cloud.geocam.a,cloud.geocam.byour own group

Output

logging.file.name=/var/log/app.logwrite to a file as welllogging.logback.rollingpolicy.max-file-size=10MBrotationlogging.logback.rollingpolicy.max-history=7days keptlogging.pattern.console=…override the console layoutlogging.structured.format.console=ecs|gelf|logstashJSON logs, no appender configlogging.console.enabled=falsesilence stdout (Boot 4)

Beyond properties

Drop logback-spring.xml on the classpath for appenders, filters and per-profile blocks (<springProfile name="prod">). Use the -spring suffix so Boot, not Logback, loads it — that is what enables <springProperty> and profile support. Log4j2 instead: exclude spring-boot-starter-logging, add spring-boot-starter-log4j2.

At runtime

GET /actuator/loggers/cloud.geocamcurrent levelPOST /actuator/loggers/cloud.geocam {"configuredLevel":"DEBUG"}change it without a restart

Testing

Plain unit test — no Spring at all

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
  @Mock OrderRepository repo;
  @InjectMocks OrderService service;

  @Test void placesOrder() {
    given(repo.save(any())).willReturn(anOrder());
    assertThat(service.place(cmd()).id()).isEqualTo(1L);
    then(repo).should().save(any());
  }
}

No context, so no startup cost and no context cache to invalidate. The cache is the thing worth understanding: Spring keys a loaded context on the full set of configuration attributes — classes, profiles, property sources, slice annotations, even which beans are replaced by @MockitoBean — and reuses it across test classes that match. Every variation you introduce is another context built from scratch, which is why a suite of 400 tests can take four seconds or four minutes on the same code.

Slices — a fraction of the context

AnnotationLoads
@WebMvcTest(OrderController.class)MVC infrastructure, controllers, advice, converters — no services or repositories. Supply them with @MockitoBean.
@WebFluxTestthe reactive equivalent, with WebTestClient
@DataJpaTestentities, repositories, an embedded database, TestEntityManager. Transactional and rolled back per test.
@JdbcTest, @DataJdbcTest, @JooqTestthe same for plain JDBC, Spring Data JDBC and jOOQ
@DataMongoTest, @DataRedisTest, @DataR2dbcTest, @DataNeo4jTest, @DataCassandraTest, @DataElasticsearchTest, @DataLdapTestone per store
@JsonTestserialisation only, with JacksonTester
@RestClientTest, @WebClientTestclient-side, with MockRestServiceServer
@GraphQlTestGraphQL controllers with GraphQlTester

Web-layer test

@WebMvcTest(OrderController.class)
class OrderControllerTest {
  @Autowired MockMvcTester mvc;
  @MockitoBean OrderService service;

  @Test void returnsOrder() {
    given(service.byId(1L)).willReturn(anOrderDto());
    assertThat(mvc.get().uri("/api/orders/1"))
        .hasStatusOk()
        .bodyJson().extractingPath("$.customer").isEqualTo("Ada");
  }
}

Whole-application test

@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureRestTestClient
class OrderApiTest {
  @Test void createsOrder(@Autowired RestTestClient client) {
    client.post().uri("/api/orders").body(newOrder())
          .exchange().expectStatus().isCreated();
  }
}
webEnvironmentEffect
MOCKdefault — mock servlet environment, no port
RANDOM_PORTreal server on a free port; inject it with @LocalServerPort
DEFINED_PORTreal server on the configured port
NONEcontext only, no web

The supporting cast

@MockitoBean / @MockitoSpyBeanreplace a bean with a mock or spy@TestConfigurationextra beans, not picked up by component scan@ActiveProfiles("test")which profile the context runs under@TestPropertySource(properties = "…")override properties@DynamicPropertySourceproperties known only at runtime — container ports@Sql("/data.sql")run SQL before the test@Transactionalroll back after each test (not for RANDOM_PORT)@DirtiesContextthrow the cached context away — slow, use sparingly

Testcontainers

@SpringBootTest
@Testcontainers
class OrderIT {
  @Container @ServiceConnection
  static PostgreSQLContainer<?> db =
      new PostgreSQLContainer<>("postgres:17");
}

@ServiceConnection wires the container’s URL, username and password into the context — no @DynamicPropertySource needed. The same containers can back spring-boot-testcontainers at development time, so ./mvnw spring-boot:test-run starts a real Postgres for local work.

Note for upgraders: Boot 4 removed @MockBean and @SpyBean. Use Spring Framework’s @MockitoBean and @MockitoSpyBean.

Actuator & Observability

Add spring-boot-starter-actuator. Only health is exposed by default, over HTTP and JMX — everything else is opt-in, deliberately.

management.endpoints.web.exposure.include=health,info,metrics,loggersexpose thesemanagement.endpoints.web.exposure.include=*expose everything — only behind authmanagement.endpoints.web.exposure.exclude=env,beans…minus thesemanagement.endpoints.web.base-path=/managedefault /actuatormanagement.server.port=9001separate port, not routed by the LB

The endpoints

IDShows
healthup/down, aggregated from every HealthIndicator. Exposed by default.
infobuild, git and custom info
metricsMicrometer meters; /actuator/metrics/{name}?tag=uri:/api/orders
prometheusthe same in Prometheus scrape format
loggersread and change log levels at runtime
env, configpropsresolved environment and bound @ConfigurationProperties — sanitised, still sensitive
beans, conditions, mappingswhat was defined, why, and every request mapping
threaddump, heapdumpa thread dump; an HPROF/PHD file
httpexchangeslast 100 request/response pairs (needs an HttpExchangeRepository)
scheduledtasks, quartzwhat is scheduled, and Quartz jobs
flyway, liquibasemigrations applied
caches, sessions, startup, auditevents, integrationgraph, logfilethe rest
shutdowngraceful shutdown. Disabled as well as unexposed — needs management.endpoint.shutdown.access=unrestricted.

Health for orchestrators

management.endpoint.health.show-details=when-authorizednever|when-authorized|alwaysmanagement.endpoint.health.probes.enabled=trueadds liveness + readiness/actuator/health/livenessrestart me if this fails/actuator/health/readinessstop sending me trafficmanagement.endpoint.health.group.db.include=db,diskSpaceyour own group

Build and git info

spring-boot-maven-plugin build-info goalwrites build-info.properties → /actuator/infogit-commit-id-maven-pluginadds commit, branch, build timemanagement.info.env.enabled=trueexpose info.* properties

Metrics and tracing

Micrometer is the facade: @Timed, @Counted, @Observed, or an injected MeterRegistry. Common tags for every meter come from management.metrics.tags.*. Tracing exports over OTLP (spring-boot-starter-opentelemetry) or Zipkin, sampled by management.tracing.sampling.probability.

Boot 4 renamed two of these: management.tracing.enabledmanagement.tracing.export.enabled, and @ConditionalOnEnabledTracing@ConditionalOnEnabledTracingExport.

Secure it. /actuator/env and /actuator/configprops leak configuration, /actuator/heapdump leaks everything in memory. Put the whole base path behind authentication, or on a management port that only your monitoring can reach.

Packaging & Running

Day-to-day commands

./mvnw spring-boot:runrun from source./mvnw spring-boot:run -Dspring-boot.run.profiles=dev…with a profile./mvnw clean packageexecutable jar in target/./mvnw spring-boot:build-imageOCI image, no Dockerfile./mvnw spring-boot:start / :stoparound integration tests./gradlew bootRun / bootJar / bootBuildImagethe Gradle equivalents

The executable jar

app.jar
├── META-INF/MANIFEST.MF        Main-Class: JarLauncher
├── org/springframework/boot/loader/…
└── BOOT-INF/
    ├── classes/                your code and resources
    ├── lib/                    every dependency, as a nested jar
    └── classpath.idx / layers.idx

Nested jars are stored, not compressed, so the launcher can read them in place. This is why an unzip-and-run does not work and why java -jar does.

Running it

java -jar app.jarthe deploymentjava -jar app.jar --server.port=9090any property, as an argumentjava -Dspring.profiles.active=prod -jar app.jar…or a system propertySPRING_DATASOURCE_PASSWORD=… java -jar app.jar…or the environment (relaxed binding)

Docker, the layered way

FROM eclipse-temurin:21-jre AS builder
WORKDIR /app
COPY target/app.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --launcher

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/app/dependencies/ ./
COPY --from=builder /app/app/spring-boot-loader/ ./
COPY --from=builder /app/app/snapshot-dependencies/ ./
COPY --from=builder /app/app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Four layers, ordered by how often they change, so a code-only rebuild ships a few hundred kilobytes instead of a hundred megabytes. Or skip the Dockerfile entirely: spring-boot:build-image uses Cloud Native Buildpacks and does the layering for you.

Native image

./mvnw -Pnative native:compileGraalVM binary; needs the native-maven-plugin./mvnw spring-boot:build-image -Pnativenative image in a container

Startup in tens of milliseconds and a fraction of the memory, at the cost of a long build and no runtime reflection you have not declared. AOT processing (process-aot) runs first and generates the hints.

Graceful shutdown

server.shutdown=gracefulfinish in-flight requests firstspring.lifecycle.timeout-per-shutdown-phase=30show long to wait

DevTools

spring-boot-devtools as an optional/developmentOnly dependency: restart on classpath change, LiveReload, template caching off, and a property defaults file at $HOME/.config/spring-boot. It disables itself in a repackaged jar, and it has no place in a production image.

Spring Boot 4: What Changed

4.0 (November 2025) is the largest reshaping since 2.0. If you are reading tutorials written for 3.x, these are the lines that will not compile.

New floors

DependencyNow
Spring Framework7.0
Spring Security7.0 — lambda DSL only
Jackson3.0 (group tools.jackson). Jackson 2 ships deprecated.
Hibernate7.1, Hibernate Validator 9.0
JakartaServlet 6.1, Persistence 3.2, Validation 3.1, WS RS 4.0
Kotlin / Gradle2.2.20 / Gradle 9 (8.14+ still supported)

The module split

The old spring-boot-autoconfigure megajar is gone. Boot now ships one module per technology, on a strict naming scheme:

spring-boot-<tech>the moduleorg.springframework.boot.<tech>its root packagespring-boot-starter-<tech>its starterspring-boot-<tech>-testits test support

What to change in the build file

WasIs
spring-boot-starter-webspring-boot-starter-webmvc
spring-boot-starter-web-servicesspring-boot-starter-webservices
spring-boot-starter-oauth2-client etc.spring-boot-starter-security-oauth2-client
bare flyway-core / liquibase-corespring-boot-starter-flyway / -liquibase
spring-boot-starter-testthe -test starter of each technology under test

Stuck? spring-boot-starter-classic and spring-boot-starter-test-classic restore the old catch-all behaviour so you can upgrade in two steps rather than one.

New capabilities

  • API versioning auto-configured for MVC and WebFlux — spring.mvc.apiversion.*, and a version attribute on the mapping annotations.
  • HTTP Service Clients auto-configured: annotate an interface with @HttpExchange, register it with @ImportHttpServices, inject the proxy.
  • RestTestClient in @SpringBootTest and @AutoConfigureMockMvc.
  • Null-safety throughout, expressed with JSpecify annotations.
  • OpenTelemetry and Kotlin Serialization starters.

Renames that will bite

WasIs
@MockBean / @SpyBean@MockitoBean / @MockitoSpyBean
org.springframework.boot.env.EnvironmentPostProcessororg.springframework.boot.EnvironmentPostProcessor
@ConditionalOnEnabledTracing@ConditionalOnEnabledTracingExport
management.tracing.enabledmanagement.tracing.export.enabled
spring.dao.exceptiontranslation.enabledspring.persistence.exceptiontranslation.enabled

Auto-configuration classes no longer expose public members other than constants — if you were subclassing or calling into one, you cannot any more. And certificate-validity-threshold support was dropped from the SSL info endpoint.

Idioms & Gotchas

Things that silently do nothing

  • Self-invocation. @Transactional, @Async, @Cacheable, @PreAuthorize and @Retryable all work through a proxy. A call from one method of a bean to another on this bypasses it entirely.
  • Non-public methods. The same annotations on a private or package-private method are ignored by the proxy.
  • A component outside the scanned package. Component scanning starts at the @SpringBootApplication class’s package. A bean in a sibling package is never found.
  • @Valid without a nested @Valid. Validation does not cascade into a collection or a nested object unless you annotate the field.
  • @EnableCaching / @EnableAsync / @EnableScheduling missing. The annotations still compile; nothing happens.

Errors and what they actually mean

MessageCause
Port 8080 was already in useanother instance, or something else. server.port=0 picks a free one.
Failed to determine a suitable driver classa JDBC starter is on the path but no spring.datasource.url. Either configure it or exclude DataSourceAutoConfiguration.
LazyInitializationExceptiona lazy association touched after the session closed. Fetch it in the query — do not re-enable open-in-view.
The dependencies of some beans form a cycleA needs B needs A. Extract the shared part into C. spring.main.allow-circular-references=true is a delay, not a fix.
No qualifying bean of type … expected single matchtwo candidates. @Primary, @Qualifier, or inject the List.
Whitelabel Error Page, 500, no detaillook at the log; the response is deliberately silent. server.error.include-message=always in dev.

Habits worth keeping

  • Never expose an entity from a controller. A DTO or a record is a contract you control; an entity drags lazy proxies, cycles and your column names into the JSON.
  • Flyway or Liquibase owns the schema. ddl-auto=validate in every environment above your laptop.
  • Constructor injection, final fields, no @Autowired.
  • Configuration in @ConfigurationProperties, not scattered @Value. Typed, validatable, discoverable.
  • Set every timeout. An HTTP client with no read timeout will eventually hang the whole pool.
  • Do not put equals/hashCode on an entity using all fields. Use the business key, or the id with a null-safe guard.
  • Secrets come from the environment, not from application.yaml in the repository.

Property Quick Reference

Application and server

spring.application.name=ordersused by tracing, metrics, Cloudserver.port=80800 = randomserver.servlet.context-path=/apiprefix every mappingserver.compression.enabled=truegzip responsesserver.shutdown=gracefuldrain in-flight requestsserver.tomcat.threads.max=200request threadsspring.threads.virtual.enabled=trueJava 21+ virtual threadsspring.main.banner-mode=offquiet startupspring.main.web-application-type=noneCLI app, no server

Datasource and JPA

spring.datasource.url=jdbc:postgresql://localhost:5432/ordersspring.datasource.username / .passwordfrom the environment in productionspring.datasource.hikari.maximum-pool-size=10poolspring.datasource.hikari.connection-timeout=3000ms to wait for a connectionspring.jpa.hibernate.ddl-auto=validatenever update in productionspring.jpa.open-in-view=falseturn this off deliberatelyspring.jpa.properties.hibernate.jdbc.batch_size=50batch writesspring.flyway.locations=classpath:db/migrationmigrationsspring.sql.init.mode=alwaysrun schema.sql / data.sql

Web and JSON

spring.mvc.problemdetails.enabled=trueRFC 9457 error bodiesspring.jackson.default-property-inclusion=non_nulldrop nulls from JSONspring.jackson.serialization.write-dates-as-timestamps=falseISO-8601spring.jackson.time-zone=UTCspring.servlet.multipart.max-file-size=10MBuploadsspring.web.resources.static-locations=…where static files come from

Operations

management.endpoints.web.exposure.include=health,info,prometheusmanagement.endpoint.health.probes.enabled=trueliveness + readinessmanagement.tracing.sampling.probability=0.110% of tracesmanagement.otlp.tracing.endpoint=http://collector:4318/v1/traceslogging.level.cloud.geocam=DEBUGlogging.structured.format.console=ecsJSON logs

Finding the rest

The reference documentation’s “Common Application Properties” appendix lists every one. Add spring-boot-configuration-processor and your IDE completes them, including your own @ConfigurationProperties. At runtime, /actuator/configprops shows what was actually bound.

Annotation Index

Grouped by what it does — hover for the full description, type in the box to filter

Bootstrapping & Configuration

25

The entry point

@SpringBootApplicationthe three-in-one entry point
@SpringBootConfigurationthe context root tests look for
@EnableAutoConfigurationguess the configuration from the classpath
@ComponentScanwhere to look for beans

Declaring configuration

@Configurationa source of bean definitions
@Beanthis method's return value is a bean
@Importbring in another configuration class
@ImportResourceload a Spring XML file
@PropertySourceadd a .properties file to the Environment
@PropertySourcesseveral @PropertySource at once

Binding properties

@ConfigurationPropertiesbind a property tree onto a type
@ConfigurationPropertiesScanfind @ConfigurationProperties classes
@EnableConfigurationPropertiesregister named properties classes
@DefaultValuedefault for a constructor-bound property
@NestedConfigurationPropertybind a nested properties object
@DeprecatedConfigurationPropertymark a property as deprecated
@Valueinject one value or expression

Profiles & ordering

@Profileonly when this profile is active
@Orderrelative position among several
@Priorityhighest precedence

Writing auto-configuration

@AutoConfigurationan auto-configuration class
@AutoConfigureBeforeorder: before those
@AutoConfigureAfterorder: after those
@AutoConfigureOrderorder: by number
@AutoConfigurationPackageregister the base package

Stereotypes & Injection

17

Stereotypes

@Componenta scanned bean
@Servicebusiness logic
@Repositorydata access, with exception translation
@Controllerweb controller returning views
@RestControllercontroller returning bodies

Wiring

@Autowiredinject by type
@Qualifierinject this named one
@Primarythe default choice
@Fallbackonly if nothing else matches
@InjectJSR-330 injection
@NamedJSR-330 named bean
@Lookupmethod injection of a prototype

Lifecycle & scope

@Scopesingleton, prototype, request…
@Lazycreate on first use
@DependsOninitialise that one first
@PostConstructafter injection
@PreDestroybefore destruction

Conditions

14

Classpath

@ConditionalOnClassthis class is on the classpath
@ConditionalOnMissingClassthis class is not
@ConditionalOnResourcethis resource exists

Beans

@ConditionalOnBeanthat bean already exists
@ConditionalOnMissingBeanthat bean does not exist
@ConditionalOnSingleCandidateexactly one candidate

Properties & expressions

@ConditionalOnPropertythis property has this value
@ConditionalOnBooleanPropertythis boolean property is set
@ConditionalOnExpressionthis SpEL is true
@Conditionalthe general mechanism

Environment

@ConditionalOnWebApplicationrunning as a web app
@ConditionalOnNotWebApplicationnot a web app
@ConditionalOnWarDeploymentdeployed as a WAR
@ConditionalOnNotWarDeploymentrunning embedded

Web MVC & REST

27

Mapping requests

@RequestMappingmap a path to a handler
@GetMappingGET
@PostMappingPOST
@PutMappingPUT
@PatchMappingPATCH
@DeleteMappingDELETE
@CrossOriginallow cross-origin requests

Reading the request

@PathVariablea segment of the URL
@RequestParama query or form parameter
@RequestBodythe request body
@RequestHeaderone header
@CookieValueone cookie
@RequestPartone part of a multipart request
@MatrixVariable;key=value in the path
@ModelAttributebind params onto an object
@RequestAttributea request attribute
@SessionAttributea session attribute
@SessionAttributeskeep model attributes in session

Writing the response

@ResponseBodyreturn value is the body
@ResponseStatusa fixed status code

Cross-cutting controller code

@ControllerAdviceshared controller behaviour
@RestControllerAdviceshared behaviour for REST
@ExceptionHandlerturn an exception into a response
@InitBindercustomise request binding

Related

@MessageMappingWebSocket / RSocket destination
@LocalServerPortthe port a test server got
@LocalManagementPortthe management port

HTTP Service Clients

8

Declaring the interface

@HttpExchangea declarative HTTP client interface
@GetExchangeGET
@PostExchangePOST
@PutExchangePUT
@PatchExchangePATCH
@DeleteExchangeDELETE
@ImportHttpServicesregister client interfaces as beans
@RSocketExchangedeclarative RSocket client

Validation Constraints

26

Triggering validation

@Validvalidate this, and cascade
@Validatedvalidate, with groups

Presence

@NotNullnot null
@Nullmust be null
@NotEmptynot null and not empty
@NotBlanknot null and not whitespace

Size & range

@Sizelength or size between
@Minat least
@Maxat most
@DecimalMinat least (decimal)
@DecimalMaxat most (decimal)
@Digitsdigit counts
@Positive> 0
@PositiveOrZero>= 0
@Negative< 0
@NegativeOrZero<= 0

Shape

@Emailemail-shaped
@Patternmatches a regex
@AssertTruemust be true
@AssertFalsemust be false

Time

@Pastin the past
@PastOrPresentpast or present
@Futurein the future
@FutureOrPresentfuture or present

Defining your own

@Constraintdeclare a custom constraint
@ReportAsSingleViolationone message for a composite

JPA & Persistence

29

The entity

@Entitya persistent class
@Tablewhich table
@Idthe primary key
@GeneratedValuehow the key is generated
@Columncolumn mapping
@Transientdo not persist this
@Enumeratedenum as STRING or ORDINAL
@Loblarge object column
@Temporallegacy date precision
@Versionoptimistic locking column
@Embeddablea value type
@Embeddedembed a value type
@EmbeddedIdcomposite key
@AttributeOverriderename an inherited column
@MappedSuperclassshared mappings, no table
@Inheritanceinheritance strategy
@DiscriminatorColumnsubtype discriminator

Associations

@OneToOne1:1
@ManyToOneN:1 — the owning side
@OneToMany1:N
@ManyToManyN:N
@JoinColumnthe foreign key column
@JoinTablethe join table
@OrderByorder a loaded collection
@ElementCollectiona collection of value types

Queries & callbacks

@NamedQuerya named JPQL query
@NamedEntityGrapha named fetch plan
@EntityListenerslifecycle listener class
@PrePersist …entity lifecycle callbacks

Spring Data Repositories

14

Queries

@Querya hand-written query
@Parambind a named parameter
@Modifyingthis query writes
@EntityGraphfetch these associations
@Locklock mode for a query
@Procedurecall a stored procedure
@QueryHintsJPA hints for a query

Auditing

@EnableJpaAuditingturn auditing on
@CreatedDatewhen it was created
@LastModifiedDatewhen it last changed
@CreatedBywho created it
@LastModifiedBywho changed it

Wiring repositories

@EnableJpaRepositoriesfind JPA repositories
@RepositoryRestResourcerepository as a REST resource

Transactions, Async & Scheduling

11

Transactions

@Transactionalrun in a transaction
@EnableTransactionManagementturn @Transactional on
@TransactionalEventListenerreact after commit

Async

@Asyncrun on another thread
@EnableAsyncturn @Async on

Scheduling

@Scheduledrun on a timer
@EnableSchedulingturn @Scheduled on
@Schedulesseveral schedules on one method

Retry

@Retryableretry on failure
@Recoverwhat to do when retries run out
@Backoffhow long to wait between tries

Caching

6

Caching

@Cacheablememoise the result
@CachePutalways run, then cache
@CacheEvictremove entries
@Cachingseveral at once
@CacheConfigclass-level cache defaults
@EnableCachingturn caching on

Security

14

Switching it on

@EnableWebSecurityturn web security on
@EnableMethodSecurityturn method security on
@EnableWebFluxSecurityreactive web security

Authorising a method

@PreAuthorizecheck before the call
@PostAuthorizecheck the return value
@PreFilterfilter the arguments
@PostFilterfilter the result
@Securedolder role check
@RolesAllowedJSR-250 role check
@AuthorizeReturnObjectsecure a returned object's methods

Reaching the current user

@AuthenticationPrincipalthe current principal
@CurrentSecurityContextthe security context

Testing

@WithMockUserrun the test as a fake user
@WithUserDetailsrun as a real loaded user

AOP & Events

12

Aspects

@Aspectdeclare an aspect
@Pointcutname a pointcut expression
@Beforerun before
@Afterrun after, always
@AfterReturningrun after a normal return
@AfterThrowingrun after a throw
@Aroundwrap the call
@EnableAspectJAutoProxyturn AOP on

Events

@EventListenerhandle an application event
@TransactionalEventListenerhandle it after commit

Null-safety (JSpecify, Boot 4)

@Nullablemay be null
@NullMarkednon-null by default

Messaging

10

Listeners

@KafkaListenerconsume a Kafka topic
@RabbitListenerconsume a Rabbit queue
@JmsListenerconsume a JMS destination
@PulsarListenerconsume a Pulsar topic

Binding the message

@Payloadthe message body
@Headerone header
@Headersall headers
@SendToroute the return value

WebSocket & RSocket

@MessageMappinga message destination
@SubscribeMappinghandle a subscription

Testing

28

Loading a context

@SpringBootTestthe whole application
@WebMvcTestMVC layer only
@WebFluxTestWebFlux layer only
@DataJpaTestJPA layer only
@DataJdbcTestSpring Data JDBC only
@JdbcTestplain JDBC only
@JooqTestjOOQ only
@DataMongoTest …one slice per data store
@JsonTestJSON serialisation only
@RestClientTestan HTTP client, with a mock server
@WebClientTesta WebClient, with a mock server
@GraphQlTestGraphQL layer only

Standing beans in

@MockitoBeanreplace a bean with a mock
@MockitoSpyBeanwrap a bean in a spy
@TestBeanreplace a bean with your own
@TestConfigurationtest-only beans
@Mock / @InjectMocksplain Mockito, no Spring

Shaping the environment

@ActiveProfilesprofiles for the test
@TestPropertySourceoverride properties
@DynamicPropertySourceproperties known only at runtime
@AutoConfigureMockMvcadd MockMvc to a full test
@AutoConfigureRestTestClientadd RestTestClient
@AutoConfigureTestDatabaseembedded DB, or not
@Sqlrun SQL around a test
@Transactionalroll the test back
@DirtiesContextdiscard the cached context

Containers

@Testcontainersmanage test containers
@ServiceConnectionwire a container into the context

Actuator & Metrics

11

Custom endpoints

@Endpointa custom Actuator endpoint
@WebEndpointHTTP only
@JmxEndpointJMX only
@ReadOperationGET
@WriteOperationPOST
@DeleteOperationDELETE
@Selectora path segment of an endpoint
@ConditionalOnAvailableEndpointonly if that endpoint is exposed

Metrics & tracing

@Timedtime this method
@Countedcount invocations
@Observedmetric and span together

Retired & Renamed

10

Gone in Spring Boot 4

@MockBean→ @MockitoBean
@SpyBean→ @MockitoSpyBean
@ConditionalOnEnabledTracing→ …TracingExport
@ConstructorBindinginferred since Boot 3

Gone earlier

@Requiredremoved in Framework 6
@EnableGlobalMethodSecurity→ @EnableMethodSecurity
WebSecurityConfigurerAdapter→ SecurityFilterChain bean
@WebIntegrationTest→ @SpringBootTest
@IntegrationTest→ @SpringBootTest
javax.* annotations→ jakarta.*