Skip to main content
Spring Boot 4 Stack Overview: Starter Granularity, MVC Version Negotiation, and Security Evolution
  1. Posts/

Spring Boot 4 Stack Overview: Starter Granularity, MVC Version Negotiation, and Security Evolution

·1854 words·9 mins
NeatGuyCoding
Author
NeatGuyCoding

Spring Boot 4 Stack Overview: Starter Granularity, MVC Version Negotiation, and Security Evolution
#

Abstract: With Spring Boot 4 and Spring Framework 7, dependencies split into finer starters by capability; outbound HTTP clients can be isolated from server-side MVC. The same codebase can enable built-in API version negotiation in Spring MVC alongside Spring Data JDBC and RestClient / declarative @HttpExchange clients. Spring Security 7 emphasizes composable Customizer<HttpSecurity>, one-time token login, WebAuthn, and annotation-driven multi-factor models; combined with JVM AOT and compile-time repository fragments for Spring Data JDBC, the startup path can move toward “train—cache—replay,” but parameters must align strictly with the JDK and module switches. The sections below walk through the dependency layers and security boundaries; example coordinates follow the current Initializr and reference manuals. When screenshots show Boot 4.1.0 (SNAPSHOT) alongside stable 4.0.x, defer to the BOM you chose.

The Initializr copy in the figure shows dependency descriptions: Spring Boot integration for RestClient and RestTemplate to make HTTP requests, and under PostgreSQL Driver, A JDBC and R2DBC driver that allows Java programs to connect to a PostgreSQL database.

The checklist includes the GraalVM Native Support blurb Support for compiling Spring applications to native executables using the GraalVM native-image, and the Spring Modulith entry Support for building modular monolithic applications.


1. Spring Boot 4: Starter reshuffle and outbound HTTP dependency separation
#

(1) Rationale
Boot 4 treats “starters split by technology” as normal: only introduce starters for technologies you need on the classpath, shrinking the surface of unrelated auto-configuration classes. A typical goal is that servlet-based web apps and “outbound HTTP only” workloads pick different starters, avoiding embedded web-container-related configuration when there is no server-side stack.

(2) Implementation levers

  • Spring Initializr dependency metadata maps dependency IDs to concrete Maven coordinates; the Initializr UI may still show legacy labels such as spring-boot-starter-web alongside Boot 4 naming evolution—projects should follow generator output for spring-boot-starter-webmvc, spring-boot-starter-restclient (dependency ID often spring-restclient), and so on.
  • Spring Boot 4.0 Migration Guide — Starters describes a more consistent starter set, with dedicated starters (and matching test starters) for most technologies.
  • Modulith: org.springframework.modulith:spring-modulith-starter-core is BOM-managed and can enforce module boundaries inside the same monolith.

(3) How to use it

<!-- Illustrative snippet: outbound client only (coordinates per Initializr output) -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-restclient</artifactId>
</dependency>

See the Boot reference chapter “Calling REST services” for wiring spring-boot-starter-restclient with the RestClient.Builder bean.

Mermaid diagram 1


2. Spring Data JDBC and a read-only HTTP API (/dogs)
#

(1) Rationale
Spring Data JDBC maps table rows around aggregate roots and suits small-to-medium domains for quick CRUD exposure. Java record types can be instantiated via constructor binding when a non-zero-arg constructor matches column names, per the JDBC mapping documentation.

(2) Implementation levers

  • Repository interfaces extend ListCrudRepository.
  • Controllers use ordinary Spring MVC annotations (@GetMapping, etc.) to expose JSON.

(3) How to use it

interface DogRepository extends ListCrudRepository<Dog, Integer> {}

@RestController
class DogsController {
  private final DogRepository dogs;
  DogsController(DogRepository dogs) { this.dogs = dogs; }
  @GetMapping("/dogs")
  Iterable<Dog> all() { return dogs.findAll(); }
}
curl -s http://localhost:8080/dogs

The browser address bar and JSON snippet show localhost:8080/dogs and [{"id":45,"name":"Prancer","owner":"josh","description":"A demonic, neurotic, man hating, animal hating.


3. Spring MVC: built-in API version negotiation
#

(1) Rationale
When multiple handler versions run in the same deployment, you need request-time version resolution, rejection of invalid versions, and hooks for deprecation notices. Spring Framework 7 exposes a unified programmatic entry point ApiVersionConfigurer; Boot maps common strategies to application properties via spring.mvc.apiversion.*.

(2) Implementation levers

Mechanism (as in docs)Boot property keys (as in appendix)
Request headerspring.mvc.apiversion.use.header
Query parameterspring.mvc.apiversion.use.query-parameter

Full key names appear in Boot Application Properties; the strategy overview is in MVC API versioning.

(3) How to use it

spring.mvc.apiversion.default=1.1
# Custom header name for demo only; align with gateway and clients in production
spring.mvc.apiversion.use.header=X-Dogs-Version

The application.properties editor shows spring.mvc.apiversion.default=1.1 and an IDE hint line for spring.mvc.apiversion.use.header starting with (Use the HTTP header th.

curl -s http://localhost:8080/dogs
curl -s -H "X-Dogs-Version: 1.0" http://localhost:8080/dogs

Routing differences between the default version and an explicit header across the two requests can be observed via version constraints on @RequestMapping and related mapping annotations; property value types follow the Boot properties appendix.


4. RestClient and declarative @HttpExchange clients
#

(1) Rationale
Outbound calls can be imperative via RestClient, or interface-driven with @HttpExchange / @GetExchange and proxies from HttpServiceProxyFactory to type remote calls. From Framework 7 onward RestTemplate is deprecated; prefer RestClient for new work.

(2) Implementation levers

(3) How to use it
Client interface (example: third-party HTTP API; replace the URI with a real endpoint):

interface CatFactsClient {
  @GetExchange("https://example.catfacts/api")
  CatFacts facts();
}

Server aggregation (avoid “client without handlers”):

@RestController
class CatsController {
  private final CatFactsClient client;
  CatsController(CatFactsClient client) { this.client = client; }
  @GetMapping("/cats")
  CatFacts cats() { return client.facts(); }
}

The source editor shows an @Import argument fragment (CatFactsClient.class) and lines such as import org.springframework.web.service.annotation.

The same project shows import org.springframework.web.service.registry and the (CatFactsClient.class) annotation near SpringApplication.run.


5. BeanRegistrar for programmatic bean registration and JSpecify @NullMarked
#

(1) Rationale
When static @Bean methods cannot express environment-driven or batched registration, BeanRegistrar incrementally declares beans in register(BeanRegistry, Environment), typically with @Import(MyRegistrar.class). At the same time, package-level @NullMarked on org.springframework.beans.factory tightens default null-safety to “non-null unless @Nullable,” aligned with the JSpecify @NullMarked definition.

(2) Implementation levers

  • BeanRegistrar, BeanRegistry.registerBean(...).
  • JSpecify intros live at jspecify.dev (annotation semantics per source/JavaDoc).

(3) How to use it

@Configuration
@Import(DynamicBeans.class)
class AppConfig {}

class DynamicBeans implements BeanRegistrar {
  @Override
  public void register(BeanRegistry registry, Environment env) {
    for (int i = 0; i < 4; i++) {
      registry.registerBean(MyRunner.class);
    }
  }
}
record MyRunner() {}

Debug source above shows registry.registerBean(MyRunner.class) and a log line Tomcat started on port 8080 (http) with context path '/'.

The structure view lists BeanRegistrar, BeanRegistry; the lower source pane includes public @interface NullMarked and the sentence For a comprehensive introduction to JSpecify, please see jspecify.org.


6. Spring Security 7: JDBC user store and runtime boundaries
#

(1) Rationale
On the Servlet stack, JdbcUserDetailsManager extends JdbcDaoImpl under UserDetailsManager semantics, driving authentication principals from database rows. Adding Security does not imply a fixed user model; you must explicitly supply UserDetailsService / UserDetailsManager and a SecurityFilterChain.

(2) Implementation levers

(3) How to use it

@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
  return http.authorizeHttpRequests(a -> a.anyRequest().authenticated())
      .httpBasic(Customizer.withDefaults())
      .build();
}
curl -s -o /dev/null -w "%{http_code}\n" -u user:pass http://localhost:8080/actuator/health

Console logs include Global AuthenticationManager configured with UserDetailsService bean with name jdbcUserDetailsManager and Tomcat started on port 8080 (http).


7. Composable Customizer<HttpSecurity>: one-time token login and WebAuthn
#

(1) Rationale
Spring Security 7 lets you assemble the DSL incrementally with Customizer<HttpSecurity> beans instead of replacing the whole default filter-chain shape. One-time token (OTT) login folds magic-link-style sign-in into oneTimeTokenLogin, with default interaction paths including /login/ott. WebAuthn / Passkeys configure RP name, RP ID, and allowedOrigins under http.webAuthn to satisfy browser ceremony origin rules.

(2) Implementation levers

  • OTT: DSL and DefaultOneTimeTokenSubmitPageGeneratingFilter details on the same manual page.
  • Passkeys: optional spring-security-webauthn dependency (Initializr mapping in dependency JSON).

(3) How to use it

@Bean
Customizer<HttpSecurity> httpSecurityCustomizer() {
  return http -> http
      .webAuthn(w -> w.rpName("spring").rpId("localhost")
          .allowedOrigins("http://localhost:8080"))
      .oneTimeTokenLogin(ott -> ott.tokenGenerationSuccessHandler((req, res, token) -> {
        res.setContentType("text/plain;charset=UTF-8");
        res.getWriter().println("please go to http://localhost:8080/login/ott?token="
            + token.getTokenValue());
      }));
}

The source window shows return HttpSecurity http — http.oneTimeTokenLogin( OneTimeTokenLoginConfigurer< and console output please go to http: //localhost:8080/login/ott?token=aa26d038-edf0-420f.

The same configuration area shows a chained fragment http.webAuthn(, Customizer<HttpSecurity> httpSecurityCustomizer(), and repeated console lines please go to http: //localhost:8080/login/ott?token=aa26d038-edf0-420f.


8. Multi-factor authentication: @EnableMultiFactorAuthentication
#

(1) Rationale
The annotation model maps “password factor + extra factor (e.g., OTT)” to framework-recognized FactorGrantedAuthority, steering users to the configured OTT login page when factors are missing. The docs state password authentication carries FactorGrantedAuthority.PASSWORD_AUTHORITY by default; noisy OCR fragments such as ONETIME TOKEN_AUTHORITY in screenshots do not match official constant names—defer to the manual and FactorGrantedAuthority.

(2) Implementation levers

(3) How to use it

@EnableMultiFactorAuthentication(authorities = { FactorGrantedAuthority.OTT_AUTHORITY })
@Configuration
class MfaConfiguration {}

The browser drives multi-step interactions; mapping factors to authorities in UserDetails belongs in your user model.

The IDE shows class CatsController, class SecurityConfiguration side by side, plus type hints for Customizer <HttpSecurity> and fragments of the HttpSecurity inheritance hierarchy.


9. JVM AOT, Spring Data JDBC repository fragments, and Leyden context
#

(1) Rationale
Boot’s JVM AOT processing precomputes initialization and caches to shorten startup; Spring Data JDBC can generate repository implementation fragments in AOT mode (official docs describe a naming template <Repository FQCN>Impl__Aot and stress it is internal optimization). OpenJDK Project Leyden aggregates AOT-related JEPs in the same optimization family as “train run—generate cache—replay.” Sample logs may show AOT cache versus module-property mismatches: align JVM arguments such as jdk.module.addmods across dump and runtime phases.

(2) Implementation levers

  • Spring Data JDBC — AOT Repositories (Asciidoc source): spring.aot.enabled, spring.aot.repositories.enabled, spring.aot.jdbc.repositories.enabled, plus guidance to supply JdbcDialect to avoid dialect probing that triggers early database access. Dialect type names follow your chosen database module docs; if a spoken example cites a specific class not listed verbatim in the public manual, treat it as not verified line-by-line in public chapters.
  • Generated class names such as DogRepositoryImpl__AotRepository may differ slightly from the documented template suffix as versions evolve; trust the current build output.

(3) How to use it

public interface DogRepository extends ListCrudRepository<Dog, Integer> {
  Collection<Dog> findByName(String name);
}

Calibrate builds and JVM flags against the Boot AOT guide and JDK release notes.

Generated source shows public class DogRepositoryImpl__AotRepository extends AotRepositoryFragmentSupport and public Collection<Dog> findByName(String name).

Terminal output includes Reading AOTConfiguration app.aot.config and writing AOTCache app.aot and [error][aot] Mismatched values for property jdk.module.addmods.

Mermaid diagram 2


10. Delivery: unzip scaffolding and verify the dependency tree
#

(1) Rationale
Sample projects unzip from Zip and build via Maven Wrapper; the IDE dependency tree checks Spring Framework patch levels, Jackson major versions, and Modulith/Security coordinates to avoid “runs locally, CI missing coordinates” failures.

(2) Implementation levers

  • Generic: unzip, ./mvnw -q -DskipTests package.
  • BOM: cross-check properties such as jackson-bom.version in spring-boot-dependencies POM against the dependency tree.

(3) How to use it

unzip -q adoptions-springio.zip && cd adoptions-springio
./mvnw -q -DskipTests package

Unzip logs show creating: adoptions-springio and inflating: adoptions-springio/pom.xml [binary].

The Maven dependency tree lists org.springframework:spring-core:7.0.6 and multiple lines for org.springframework.modulith:spring-modulith-s.


References and further reading
#

Related

Gradually Adopting Spring Boot for Legacy Servlet Apps: Build, Auto-Configuration, and Dual WAR Modes

·2089 words·10 mins
Before a large-scale Spring Boot migration, establish repeatable integration verification and a controlled dependency baseline; then advance in layers—Starter, auto-configuration troubleshooting, Spring context inside an external container, transitional Holder, beanification, Servlet annotation migration, and executable WAR. The article is organized by dependency and runtime layers, contrasts demo-style bootstrap paths with the reference manual’s recommended path, and where official docs do not spell out behavior (for example process lifecycle when only a non-web context starts), leaves engineering-level uncertainty explicit.

Supercharging Spring Boot Tests with Kotlin Expressiveness: Assertions, Fixtures, and Reactive Boundaries

·1833 words·9 mins
Spring Boot and Kotlin interoperate maturely on the JVM; teams often introduce Kotlin first in src/test, applying extension functions, default parameters, type-safe DSLs, and assertion styles such as Kotest in integration tests and MockMvc scenarios to cut boilerplate and tighten failure messages. Meanwhile, Java builders, overloaded static helpers, and Project Reactor’s StepVerifier each carry their own cognitive cost; the article organizes common motivations by dependency layer, alignable public APIs, and semantic boundaries to watch (e.g. JVM type erasure, whether reactive verification truly completes subscription).

AI Coding Agents on Spring Projects: Live Pipelines, Verifiable Loops, and Context Governance

·2826 words·14 mins
For engineers already shipping services on the JVM and web stacks, this article starts from a typical Spring Boot + Kotlin real-time interactive application, traces the data path from database signals through reactive SSE to the browser, and breaks human–agent collaboration into three verifiable layers: compile-and-test closure, versioned project memory (CLAUDE.md, rules, Skills), and Hooks plus MCP on the tool-call path. The second half covers gaps in tests and state machines caused by spec-less iteration, how structured clarification (Interview) writes navigation and security decisions into the specification, and how cross-cutting steps can be silently dropped in long conversations—with ideas for segmented execution.