Spring Boot

Spring Boot

Advanced Spring Boot notes for the Spring Boot 4 and Spring Framework 7 generation, covering auto-configuration, dependency injection, MVC/REST, Jakarta Persistence, transactions, security, testing, HTTP clients, virtual threads, observability, Native Image, and production reliability.

Spring Boot provides a coherent runtime and build model for applications built on the Spring Framework. Its value is not limited to creating HTTP endpoints with a few annotations. It coordinates dependency injection, external configuration, data access, transaction boundaries, security, testing, operational endpoints, packaging, and deployment around a compatible dependency set.

Spring Boot 4.1.1 requires Spring Framework 7.0.9 or later, runs on Java 17 or later, and its documented compatibility range extends through Java 26. Java 25 LTS is a suitable baseline for the examples. Java 27 has been released, but Spring Boot 4.1.1 does not yet document Java 27 in its supported range. Upgrades should evaluate starter names, Jakarta packages, JSON libraries, test modules, and third-party integrations together with application code.

1. Spring Framework and Spring Boot

Spring Framework supplies the IoC container, dependency injection, AOP, transactions, web stacks, data-access abstractions, and integration facilities. Spring Boot does not replace those facilities. It adds opinionated defaults, compatible dependency management, auto-configuration, executable packaging, and operational conventions.

Spring Framework = programming model + infrastructure abstractions
Spring Boot      = opinionated bootstrap + auto-configuration + dependency alignment + operations

Boot defaults are not mandatory. Many auto-configurations back off when the application provides its own bean or explicit configuration. Conditions such as @ConditionalOnClass, @ConditionalOnMissingBean, and @ConditionalOnProperty implement that behavior.

2. Spring Boot 4 project baseline

Spring Boot 4 modularized its starter and auto-configuration structure. For new MVC applications, spring-boot-starter-webmvc is the preferred starter; the older spring-boot-starter-web remains for compatibility but is deprecated.

A minimal Maven service can be defined as follows:

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

<properties>
  <java.version>25</java.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>
</dependencies>

When Boot dependency management is used, arbitrary individual version overrides should be avoided. A security-driven override may be justified, but its compatibility impact should be documented and tested.

3. Application entry point and package boundary

A typical application starts with @SpringBootApplication:

package org.example.orders;

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

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

Keeping the main class in the application root package gives component and entity scanning a natural boundary. Broad scanBasePackages settings can make module ownership less explicit and should not replace a clear package structure.

4. IoC, beans, and dependency injection

Spring manages the object graph inside an ApplicationContext. Stereotypes such as @Service, @Repository, @Controller, and @RestController communicate architectural intent as well as registering components.

Constructor injection is the preferred default:

@Service
public final class PriceService {
    private final TaxPolicy taxPolicy;

    public PriceService(final TaxPolicy taxPolicy) {
        this.taxPolicy = taxPolicy;
    }

    public BigDecimal gross(final BigDecimal net) {
        return net.add(taxPolicy.taxFor(net));
    }
}

It makes required dependencies explicit, keeps instances valid after construction, and permits straightforward testing without a Spring context. When multiple implementations exist, @Qualifier, @Primary, or an explicit domain factory can resolve the selection deliberately.

5. Auto-configuration

Auto-configuration reacts to classpath contents, existing beans, configuration properties, and application type. Spring Boot 4 distributes auto-configuration across more focused technology modules, so applications should not depend on the package layout of the former monolithic model.

To understand an unexpected configuration, inspect conditional annotations, configuration-property metadata, the condition evaluation report, and explicit application beans. Starter dependencies often provide both libraries and the conditions that make corresponding auto-configuration eligible.

6. External configuration and typed properties

Endpoints, timeouts, feature flags, and environment-specific options should not be hard-coded. Spring Boot merges property sources according to a defined precedence.

Related properties can be bound to a type-safe model:

package org.example.orders.config;

import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("orders.remote")
public record RemoteOrderProperties(String baseUrl, Duration connectTimeout, Duration readTimeout) {
}
@Configuration
@EnableConfigurationProperties(RemoteOrderProperties.class)
class OrderConfiguration {
}
orders:
  remote:
    base-url: https://api.example.test
    connect-timeout: 2s
    read-timeout: 5s

Passwords, tokens, and private keys should not be committed as ordinary application configuration. Secret storage must follow the security model of the deployment environment.

7. Profiles and environment separation

Profiles can activate property sets or bean definitions for development, test, and production. They should not become a substitute for domain policy. Business behavior is clearer when modeled explicitly rather than branching on an environment name.

Running the same immutable binary in multiple environments with only external configuration improves release integrity and rollback behavior.

Ahead-of-time and native-image processing imposes additional constraints on runtime-dynamic bean graphs and profile-dependent structures; native deployments should be validated independently.

8. Spring MVC request processing

The Servlet-based MVC stack routes requests through DispatcherServlet, handler mappings, argument resolvers, controllers, message converters, and response handling.

@RestController
@RequestMapping("/api/orders")
public final class OrderController {
    private final OrderService service;

    public OrderController(final OrderService service) {
        this.service = service;
    }

    @GetMapping("/{id}")
    public OrderResponse find(@PathVariable final long id) {
        return service.find(id);
    }
}

A controller should represent the HTTP boundary rather than acting as a persistence layer. Application services own use cases; repositories own persistence concerns.

9. HTTP contracts and REST design

A REST contract consists of URI design, method semantics, status codes, headers, and representation formats. GET is used for retrieval, POST generally creates or initiates processing, PUT is normally idempotent replacement at a known resource, PATCH describes partial modification, and DELETE removes a resource according to the application contract.

public record CreateOrderRequest(@NotBlank String customerCode,
                                 @NotEmpty List<@Valid OrderLineRequest> lines) {
}

public record OrderLineRequest(@NotBlank String productCode,
                               @Positive int quantity) {
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderResponse create(@Valid @RequestBody final CreateOrderRequest request) {
    return service.create(request);
}

External DTOs should not be conflated with persistence entities. Exposing an entity directly can leak lazy relationships, internal fields, recursive graphs, and persistence-driven changes into a public API.

10. JSON in Boot 4: Jackson 3

Jackson 3 is the preferred default JSON library in Spring Boot 4. Jackson 2 support is retained only as a migration path and is deprecated for future removal within the Boot 4 line.

Migration from Boot 3 must inspect package changes, custom serializers, modules, configuration, and transitive dependencies. API compatibility also depends on nullability, unknown fields, date/time formats, and enum evolution, not merely on Java type compatibility.

XML can still be supported through content negotiation and message converters when required, but representation-specific security and data-exposure behavior must be reviewed.

11. Validation and domain rules

Jakarta Bean Validation is appropriate for boundary constraints such as required fields, lengths, numeric ranges, and syntactic formats. Rules that depend on persisted state or business context belong in application/domain services.

public record RegisterCustomerRequest(
        @NotBlank @Size(max = 80) String name,
        @Email @Size(max = 254) String email) {
}

Validation itself needs resource limits. Extremely large nested payloads can consume substantial CPU and memory even when every field is formally valid.

12. Error handling and Problem Details

An HTTP error response should not expose a stack trace. Stable error codes, safe human-readable messages, and optional correlation identifiers are usually more useful to clients.

Spring Framework supports ProblemDetail for RFC 9457-style responses:

@RestControllerAdvice
public final class ApiExceptionHandler {
    @ExceptionHandler(OrderNotFoundException.class)
    public ResponseEntity<ProblemDetail> notFound(final OrderNotFoundException ex) {
        ProblemDetail detail = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
        detail.setTitle("Order not found");
        detail.setProperty("code", "ORDER_NOT_FOUND");
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(detail);
    }
}

SQL text, filesystem paths, secrets, and internal implementation details belong in protected diagnostic channels rather than API responses.

13. Spring Data JPA and Jakarta Persistence

Spring Data JPA 4.x provides repository support for Jakarta Persistence. Legacy javax.persistence imports must be migrated to jakarta.persistence in current Spring generations.

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

    @Column(nullable = false, length = 40)
    private String customerCode;

    @Version
    private long version;

    protected OrderEntity() {
    }

    public OrderEntity(final String customerCode) {
        this.customerCode = customerCode;
    }
}

Repositories remove boilerplate but not database cost. N+1 queries, inappropriate fetch plans, oversized entity graphs, uncontrolled pagination, and unnecessary dirty checking remain production concerns.

public interface OrderRepository extends JpaRepository<OrderEntity, Long>, JpaSpecificationExecutor<OrderEntity> {
}

Derived method names are useful for small queries; complex conditions may be clearer as Specifications, explicit JPQL, or another query model suited to the domain.

14. Transaction boundaries

@Transactional usually applies through proxy/AOP interception. The transaction boundary should match an application use case rather than the entire HTTP request.

@Service
public class OrderCommandService {
    private final OrderRepository repository;

    public OrderCommandService(final OrderRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public long create(final String customerCode) {
        return repository.save(new OrderEntity(customerCode)).getId();
    }
}

Important details include self-invocation through proxies, rollback rules, database isolation, lock duration, retry semantics, and the interaction between transactions and remote calls.

Waiting for a remote HTTP service while holding database locks is usually a poor reliability and latency trade-off.

15. Schema migration

ddl-auto=update is a development convenience rather than a controlled production migration strategy. Schema changes should be versioned and deployed as explicit migrations.

Spring Boot integrates with Flyway and Liquibase through technology-specific modules/starters. A migration should be source-controlled and designed for the rollout model.

A common zero-downtime pattern is:

expand -> deploy compatible code -> migrate data -> remove old usage -> contract

This avoids forcing old and new application versions to depend on mutually exclusive schemas during rolling deployments.

16. Spring Security

Spring Security applies authentication, authorization, CSRF, session, header, and other controls through its filter chain. Current configuration uses a SecurityFilterChain bean:

@Configuration
@EnableMethodSecurity
public class SecurityConfiguration {
    @Bean
    SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/health").permitAll()
                        .anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults())
                .build();
    }
}

Method authorization is not enabled merely by adding the security starter. @EnableMethodSecurity activates it, and @PreAuthorize is the current expressive authorization mechanism:

@PreAuthorize("hasAuthority('order:read')")
public OrderResponse find(final long id) {
    return queryService.find(id);
}

Successful authentication does not imply access to every business object. Object ownership, tenant boundaries, and privileged operations need explicit authorization rules.

17. CSRF, CORS, and session models

CORS is a browser response-sharing policy, not authentication or authorization. CSRF is relevant when ambient credentials such as cookies are automatically attached to state-changing requests.

A server-side session application and a stateless bearer-token API therefore require different security reasoning. Disabling CSRF merely because an endpoint is called “REST” is not a sufficient analysis.

18. HTTP clients: RestClient, WebClient, and HTTP Service Clients

Spring Framework 7 deprecates RestTemplate in favor of RestClient for new synchronous code. WebClient remains the non-blocking/reactive option. HTTP Service Clients provide interface-based contracts over these clients.

@Bean
RestClient inventoryClient(final RestClient.Builder builder, final RemoteOrderProperties properties) {
    return builder.baseUrl(properties.baseUrl()).build();
}
public interface InventoryApi {
    @GetExchange("/api/stock/{productCode}")
    StockResponse get(@PathVariable String productCode);
}

Timeouts, connection reuse, retries, response-size limits, idempotency, and remote capacity are part of the client contract. Critical services should not rely on unspecified timeout behavior.

19. Blocking, reactive, and virtual-thread execution

Spring MVC works naturally with blocking code. WebFlux uses a Reactor-based non-blocking model. Reactive code is not universally faster; it trades a different execution model for better utilization in particular high-concurrency I/O workloads.

Spring Boot can enable Java virtual threads:

spring.threads.virtual.enabled=true
spring.main.keep-alive=true

Virtual threads require Java 21+, and Spring Boot documentation recommends Java 24+ for the best experience. Traditional thread-pool properties no longer represent the same limits when virtual threads are enabled, and pinned virtual threads must still be diagnosed.

Virtual threads do not create infinite downstream capacity. A database connection pool of fifty connections remains a concurrency bottleneck regardless of how many virtual threads are waiting.

20. Caching and consistency

Spring Cache provides an abstraction through annotations such as @Cacheable, @CachePut, and @CacheEvict. Cache invalidation remains a domain-specific consistency decision.

Cache keys must include dimensions that affect the result, such as tenant, locale, authorization context, or version when applicable. A TTL is a statement about acceptable staleness, not merely a performance setting.

Local caches can diverge across application instances. Before introducing a cache, measure whether the actual bottleneck is query design, indexing, serialization, or remote latency.

21. Messaging and asynchronous workflows

Kafka, AMQP/RabbitMQ, and other brokers can decouple request-response flows. Messaging does not automatically guarantee correctness. Delivery semantics, idempotent consumers, ordering, poison messages, and dead-letter handling require explicit design.

A database transaction and a broker publish are separate resources unless a coordinated protocol is used. The outbox pattern is one way to persist business state and the intent to publish within the same database transaction.

22. Thymeleaf and server-side HTML

Thymeleaf remains a valid server-side rendering option. It can be operationally simpler than a separate SPA for applications whose interaction model does not require a client-heavy architecture.

View models are preferable to exposing persistence entities directly to templates. Escaping should remain enabled for untrusted content; rendering raw user-controlled HTML can create XSS vulnerabilities.

23. Test layers

Not every Spring test requires @SpringBootTest. Test scope should match the behavior being verified:

  • unit tests without Spring,
  • MVC slices with @WebMvcTest,
  • persistence slices with @DataJpaTest,
  • full integration with @SpringBootTest,
  • real infrastructure dependencies through Testcontainers and @ServiceConnection.

Spring Boot 4 has technology-specific test starters, so migration should review the test dependency set as well as production starters.

@WebMvcTest(OrderController.class)
class OrderControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private OrderService service;

    @Test
    void missingOrderReturns404() throws Exception {
        when(service.find(42L)).thenThrow(new OrderNotFoundException(42L));
        mockMvc.perform(get("/api/orders/42"))
                .andExpect(status().isNotFound());
    }
}

Mocking every external boundary cannot validate ORM mappings or database-specific SQL behavior. Critical persistence code needs integration coverage against a representative database engine.

24. Actuator and observability

Actuator can expose health, info, metrics, loggers, and other operational endpoints. Enabling an endpoint and exposing it over the network are separate decisions, and operational endpoints need their own authorization policy.

Spring Boot describes observability in terms of logs, metrics, and traces, with Micrometer Observation supporting metrics and tracing. Cardinality is an operational cost. Unbounded user IDs, request IDs, or free-form strings should not be used as metric labels without deliberate controls.

Liveness and readiness should also be modeled correctly. Process liveness should not necessarily depend on every remote service, otherwise a remote outage can trigger restart storms.

25. Packaging and deployment

Executable JAR is the common modern deployment model. WAR deployment remains available for environments that require an external Servlet container.

Container deployment should consider immutable images, non-root execution, memory limits, graceful shutdown, readiness/liveness separation, dependency provenance, and SBOM generation.

Spring Boot build plugins can create OCI images through buildpacks.

26. GraalVM Native Image and AOT

Spring Boot supports GraalVM Native Image. Native executables can reduce startup time and runtime memory footprint, but they increase build-time analysis and impose closed-world constraints.

Reflection, dynamic proxies, resource loading, and serialization may require reachability hints. Native deployment should be tested explicitly rather than assuming JVM test success is sufficient.

A long-running high-throughput JVM can benefit from JIT optimization, while short-lived workloads may benefit more from fast native startup. The choice should follow measurement rather than fashion.

27. Performance engineering

Spring dispatch overhead is often smaller than database, network, allocation, and contention costs. A useful latency model is:

database query
remote-service RTT
connection-pool wait
serialization
lock contention
GC/allocation
thread or event-loop queue

Optimize the measured bottleneck. Unlimited result sets, uncontrolled retries, oversized JSON documents, and accidental entity graphs usually matter more than replacing one annotation with another.

Virtual threads, reactive programming, and native images solve different classes of problems; none automatically repairs a poor data model or remote-service contract.

28. Reliability and fault tolerance

A remote call without a bounded timeout can consume resources indefinitely. A retry on a non-idempotent operation can create duplicate orders or payments.

A robust policy considers connection/read/overall timeouts, bounded retries with backoff, idempotency keys, concurrency limits, circuit breakers where justified, fallback correctness, and backpressure.

A fallback must not fabricate success unless stale or partial data is explicitly part of the business contract.

29. Layer boundaries and non-transactional side effects

A useful dependency direction is:

HTTP adapter -> application service -> domain -> repository / external adapter

This is not a mandatory package template. Its purpose is to keep domain decisions from being unnecessarily coupled to HTTP, JPA, or JSON frameworks.

Email, HTTP, or broker calls inside a database transaction create independent failure domains that are not automatically atomic. Outbox/event/orchestration patterns can make those boundaries explicit.

30. Migrating Boot 2/3 material to Boot 4

Older teaching material can remain conceptually useful while its APIs become obsolete. A migration review should include:

  • modern Jakarta packages instead of legacy javax.*,
  • spring-boot-starter-webmvc for new MVC projects,
  • Jackson 3 migration,
  • SecurityFilterChain instead of legacy security adapters,
  • @EnableMethodSecurity instead of @EnableGlobalMethodSecurity,
  • RestClient for new synchronous HTTP client code,
  • a migration plan for RestTemplate,
  • Boot 4 technology-specific starters and test starters,
  • AOT restrictions when building native images.

The target is behavioral equivalence, not merely successful compilation. Serialization, security filters, validation, transactions, and schema migration behavior need regression tests.

31. Compact end-to-end service example

public record CreateDeviceRequest(@NotBlank String serialNumber) {
}

public record DeviceResponse(long id, String serialNumber) {
}
@Entity
@Table(name = "device")
class DeviceEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 64)
    private String serialNumber;

    protected DeviceEntity() {
    }

    DeviceEntity(final String serialNumber) {
        this.serialNumber = serialNumber;
    }

    Long id() {
        return id;
    }

    String serialNumber() {
        return serialNumber;
    }
}
interface DeviceRepository extends JpaRepository<DeviceEntity, Long> {
}
@Service
class DeviceService {
    private final DeviceRepository repository;

    DeviceService(final DeviceRepository repository) {
        this.repository = repository;
    }

    @Transactional
    DeviceResponse create(final CreateDeviceRequest request) {
        DeviceEntity entity = repository.save(new DeviceEntity(request.serialNumber()));
        return new DeviceResponse(entity.id(), entity.serialNumber());
    }
}
@RestController
@RequestMapping("/api/devices")
class DeviceController {
    private final DeviceService service;

    DeviceController(final DeviceService service) {
        this.service = service;
    }

    @PostMapping
    ResponseEntity<DeviceResponse> create(@Valid @RequestBody final CreateDeviceRequest request) {
        DeviceResponse response = service.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
}

Caching, messaging, security, and remote clients are intentionally absent. Infrastructure should be added only when a concrete requirement justifies it.

Spring Boot builds on Java Programming. HTTP and browser/server fundamentals are covered in Web Programming. Language/runtime trade-offs are discussed in Programming Languages, while architectural quality belongs with Software Engineering.

References

  1. Spring, Spring Boot Reference Documentation — 4.1.x. https://docs.spring.io/spring-boot/
  2. Spring, Spring Boot System Requirements. https://docs.spring.io/spring-boot/system-requirements.html
  3. Spring, Spring Framework Reference Documentation — 7.0.x. https://docs.spring.io/spring-framework/reference/
  4. Spring, Spring Security Reference — Method Security. https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html
  5. Spring, Spring Data JPA Reference Documentation. https://docs.spring.io/spring-data/jpa/reference/
  6. Spring, Spring Boot JSON Support — Jackson 3. https://docs.spring.io/spring-boot/reference/features/json.html
  7. Spring, Spring Boot Testcontainers and Service Connections. https://docs.spring.io/spring-boot/reference/testing/testcontainers.html
  8. Spring, Spring Boot Observability. https://docs.spring.io/spring-boot/reference/actuator/observability.html
  9. Spring, GraalVM Native Images with Spring Boot. https://docs.spring.io/spring-boot/how-to/native-image/
  10. Oracle, Java SE Downloads and Release Information. https://www.oracle.com/java/technologies/downloads/
Contents
QR code for this page