Spring Boot

Spring Boot
QR code for this page

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.

Unit 1: Spring Boot Foundations, Project Model, and Configuration

1.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.

1.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.

1.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.

1.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.

1.5. Auto-configuration

Spring Boot auto-configuration condition evaluation and user-defined bean backoff

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.

1.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.

1.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.

Unit 2: Web Layer, HTTP Contracts, and Validation

Web-request handling begins by separating transport representation from business rules. Define input contracts and validation before specifying authorization, transaction execution, and response mapping. Invalid client data and unexpected server failures should not collapse into the same response semantics. Endpoint correctness includes consistent failure status and the absence of unintended side effects, not only successful JSON output.

2.1. Spring MVC Request Processing

A Spring MVC request passing through filters, the DispatcherServlet, interceptor preHandle calls in order and the controller, then unwinding with afterCompletion in reverse order
Spring MVC request chain

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

HTTP request
    |
    v
Filter chain
    |
    v
DispatcherServlet
    |
    v
HandlerMapping -> Controller -> Service -> Repository
                                     |
                                     v
                                  database
    |
    v
HTTP response
@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.

2.2. 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.

2.3. 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.

2.4. 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.

2.5. 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.

Unit 3: Persistence, Transactions, and Schema Management

A persistence workflow starts by defining the operations that must complete in one transaction. Evaluate read and write boundaries alongside isolation level and database constraints. A modified ORM object in memory does not mean the change has been durably committed; distinguish flush from commit. Where concurrent updates may overwrite one another, verify version checking or locking against concrete conflict scenarios.

3.1. 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.

3.2. Transaction Boundaries

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

Controller
    |
    v
@Transactional Service
    |
    +--> JPA / JDBC --> database
    |                    |
    |                    +--> COMMIT
    |                    +--> ROLLBACK
    |
    +--> remote service: design the transaction boundary deliberately
@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.

Separate request lifetime from transaction lifetime. Keeping a transaction open for the entire web request commonly assigns the wrong ownership boundary. Retaining a JDBC connection or row lock while waiting for a remote HTTP service, broker acknowledgement, or slow client increases contention under load. A short transaction should enclose only the read/modify/write operations that must be atomic; external effects need their own retry, idempotency, or outbox protocol.

With proxy-based Spring transaction management, a direct invocation of another method on the same object may bypass @Transactional advice. Starting a virtual thread or crossing an @Async boundary does not automatically carry thread-bound transaction context either. Instead of reasoning solely from annotations, trace when the physical connection is obtained, where commit or rollback actually occurs, and what externally visible effect remains after a partial failure.

3.3. 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.

Unit 4: Security, Integration, and Execution Models

4.1. Spring Security

OAuth 2 authorization code flow with PKCE code verifier and code challenge
OAuth 2 with PKCE

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.

4.2. 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.

4.3. 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.

4.4. 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.

blocking model:       request -> thread --------> blocking I/O -> response
virtual-thread model: request -> virtual thread -> blocking I/O -> response
reactive model:       request -> event loop ----> nonblocking I/O -> signal

model choice -> workload + library behavior + backpressure requirements

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.

4.4.1. Admission Control Is Still Required with Virtual Threads

Enabling virtual threads in Spring Boot can let blocking controller and service code carry more concurrent requests with much lower thread cost. It does not automatically expand the capacity of the JDBC pool, a remote HTTP service, the file system, or CPU-bound work downstream.

Two separate questions therefore have to be answered at an application boundary:

Which execution model carries the task?
              !=
How many tasks may enter the scarce resource concurrently?

The second answer may be a connection pool, semaphore, bounded queue, or another admission-control mechanism. If hundreds of virtual threads converge on the same expensive remote call, a finite permit budget in front of the call can protect both local memory and the remote service. Permit waiting should also be bounded; timeout and rejection behavior belong to the HTTP contract.

This is not simply a renamed servlet thread-pool setting. The goal is not to make execution threads artificially scarce again, but to model the resource that is actually scarce. Different resources may need different limits; database and remote-service work do not have to share one concurrency budget.

Observing active permits, permit-wait time, timeouts, rejections, and downstream latency together makes it easier to distinguish an execution-model problem from dependency saturation. This is the Spring-level counterpart of the connection-pool and queueing material in High-Performance Java Data Systems.

An execution model needs a capacity argument. Virtual threads can reduce platform-thread occupancy for many waiting tasks, but they do not increase the number of database connections, an upstream service's rate limit, or CPU cores. WebFlux does not guarantee superior throughput either: running blocking JDBC operations on an event loop can propagate stalls. Compare execution models using equivalent handler semantics, database plans, serialization work, and failure conditions.

Do not report RPS in isolation. At each load step, capture in-flight requests, JDBC acquisition delay, CPU use, allocation rate, p99, timeouts, and platform/virtual-thread counts. If an implementation holds more waiting requests in memory but completes no more business operations, it has moved the queue rather than expanded capacity. Admission control and an explicit timeout budget remain necessary regardless of execution model.

4.5. 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.

A cache key built only from query parameters is unsafe when the result depends on the requester's authorization. Two users looking up the same record may legitimately see different fields. Cache design must account for dataset version, authorization scope, and other output-affecting dimensions such as locale. The interval between revoking access and invalidating a cached result is a security requirement, not merely a freshness preference. A TTL alone does not prove the requirement is met; authorization changes and invalidation behavior must be tested together.

4.6. 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.

4.7. Cryptographic Keys and Configuration Boundaries in Services

Cryptographic safety in a Spring Boot service does not follow merely from a library supporting AES/GCM. How a key is provisioned, whether it enters logs, who may read it, and how rotation affects historical records are central concerns. Service configuration should specify algorithm version, key reference, nonce policy, and tenant trust boundaries. Binding a record identifier to AAD does not replace application authorization.

TLS certificate verification for WebClient connections must not be disabled; trust-store configuration and hostname checks need regression testing across upgrades. Fixed keys from test fixtures must never enter production. The primitives are developed in Cryptology; this course covers their execution and configuration within the Spring lifecycle.

Unit 5: Presentation, Testing, Observability, and Deployment

5.1. 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.

5.2. 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.

5.3. 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.

5.4. 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.

5.5. 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.

Unit 6: Performance, Reliability, and Version Migration

6.1. Performance Engineering

Queueing model showing latency growth as arrival rate approaches service rate together with Little law and utilization
Queueing saturation and Littles law

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.

Every tuning change carries a burden of proof. Altering the default connection pool, JSON mapper settings, or HTTP compression is not itself an optimization. A useful hypothesis states a measurable mechanism: if transferring large JSON responses dominates, separate server CPU time, serialized bytes, transfer time, and client parsing cost. Compression can trade network time for CPU work, and its value depends on payload and workload distributions.

Keep non-code variables stable, warm up the JVM, compare against the same dataset, and preserve both error behavior and response semantics. A performance claim should include p95/p99, resource cost per completed operation, and observed run-to-run variation rather than average response time alone. When an experiment has not been executed, describe the expected effect as a hypothesis, not a measured gain.

6.2. Reliability and Fault Tolerance

Retries of 300 clients that all failed together forming synchronized waves under fixed delay and exponential backoff while full jitter spreads the load and lowers the peak
Retry backoff and jitter

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.

6.3. 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.

6.4. 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.

6.5. Modeling Capacity as a Chain in Spring Boot Services

Spring Boot service capacity is bounded by the dependency chain from HTTP admission to transaction completion. The application may accept more concurrent requests while HikariCP connections, query plans, or downstream services remain fixed bottlenecks. Connection-acquisition time, transaction duration, and response queueing should therefore be measured separately. Timeouts, cancellation, and controlled load rejection need validation before allowing waiting work to grow without bounds.

  • Model HTTP worker or virtual-thread capacity together with connection-pool and database concurrency limits.
  • Measure how retries can multiply load during overload.
  • Relate timeout values to service-time distributions and downstream SLOs.

6.6. Joint Design of Virtual Threads, JDBC Pools, and Admission Control

Virtual threads may reduce scheduling overhead for blocking Java requests, but they do not enlarge JDBC connection counts, database sessions, concurrent SQL capacity, or remote I/O limits. A service capable of creating thousands of virtual threads can still accumulate requests waiting on a smaller HikariCP pool. Capacity planning must rely on acquisition time, transaction duration, and sustainable database service rate rather than thread count.

Connection acquisition timeouts, request deadlines, and admission control share one load budget. A larger pool does not necessarily increase throughput when database contention or I/O waits grow. Load tests compare active, idle, and pending connections with acquisition p95/p99, transaction time, error rates, and database waits. Retry policy must not create a secondary burst when the pool is saturated.

Unit 7: End-to-End Application and Behavioral Verification

7.1. 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.

7.3. Blocking Boundaries, Transaction Scope, and Remote Calls

@Transactional does not make an entire business workflow atomic; it creates a transaction boundary only for resources managed by the configured transaction manager. A database update and a remote HTTP call inside the same method are not automatically one ACID transaction.

Waiting on a remote service while a database transaction remains open can also hold connections and locks unnecessarily, amplifying pool exhaustion and contention when latency rises.

When local data changes must coordinate with external side effects, patterns such as an outbox, idempotent consumers, or sagas can make partial-failure behavior explicit without pretending all systems share one transaction.

7.4. Tying Spring Behavior to Framework Contracts

Spring Boot auto-configuration depends on version and classpath. A bean should not be assumed to appear “automatically”; condition evaluation and the official reference documentation can show which condition activated it.

@Transactional, security filter chains, and HTTP-client behavior depend on proxy and scope boundaries. Self-invocation, thread changes, and remote calls should be tested when they can cross those boundaries.

A production-oriented test strategy needs more than contextLoads. Database, serialization, timeout, authorization, and error contracts benefit from integration tests close to the real protocol, while critical configuration should have explicit property/bean checks.

Unit 8: Deepening Spring Container, Proxy, and Transaction Semantics

A Spring Boot application's behavior cannot be understood from annotations alone. The effects of @Service, @Transactional, @Cacheable, or @Async depend on bean lifetime, proxy boundaries, the object through which a call travels, and the thread or context in which execution continues.

8.1. Bean Scope Is a Lifetime Contract

Spring's default bean scope is singleton. This is not identical to the classic Java Singleton pattern. It means roughly one shared instance per bean definition inside the relevant ApplicationContext.

Other scopes can include:

singleton
 -> shared for the context lifetime

prototype
 -> a new instance for each resolution

request
 -> HTTP request scope

session
 -> HTTP session scope

Scope does not automatically solve thread safety. A mutable request-specific field inside a shared singleton service can be accessed by concurrent requests.

Typical service beans should therefore prefer stateless behavior, immutable dependencies, and request state passed through method parameters.

8.2. Injecting short-lived Beans into long-lived Beans

If a prototype bean is resolved directly into a singleton constructor, the resolved instance can remain held for the singleton's entire lifetime. Prototype does not magically mean “new on every method call.”

When a fresh instance is truly required at use time, a provider or factory can make the lifecycle boundary explicit:

singleton
   ↓
ObjectProvider / factory
   ↓ on demand
prototype instance

The same lifecycle mismatch can appear when request/session-scoped objects are referenced by longer-lived beans, motivating scoped proxies or providers.

Dependency Injection is therefore partly a problem of binding compatible lifetimes, not merely locating objects.

8.3. Bean Lifecycle and Resource Ownership

The Spring singleton bean lifecycle from constructor and dependency injection through Aware and init callbacks and proxy wrapping to the destroy callbacks at shutdown
Spring bean lifecycle

A useful conceptual lifecycle is:

instance creation
  ↓
dependency injection
  ↓
post-processors
  ↓
initialization callbacks
  ↓
runtime use
  ↓
destruction callbacks

@PostConstruct should not automatically become a home for expensive remote calls or large blocking initialization. A failure there can prevent the entire context from starting.

Likewise, shutdown callbacks such as @PreDestroy are useful for cleanup but should not be the sole correctness mechanism for persistent state because forced process termination can bypass graceful shutdown.

8.4. The Proxy Boundary Is Where Advice Happens

Spring proxy call running the advice while a self-invocation through this bypasses the proxy
Proxy boundary and self-invocation

Spring AOP and declarative transaction management commonly intercept calls through proxies:

caller
  ↓
Spring proxy
  ↓
interceptor / advice
  ↓
target method

A call that never crosses the proxy may bypass the advice.

Classic self-invocation:

@Service
class BillingService {

    public void outer() {
        inner(); // same target object
    }

    @Transactional
    public void inner() {
        // ...
    }
}

Even if outer() is invoked through a proxy, the call to inner() occurs through this on the target instance. Proxy-based transaction advice may therefore not run as expected.

The clean fix is often to make the transaction boundary a separate public use-case bean rather than locating the proxy manually.

8.5. JDK Proxies versus class-based Proxies

Spring can use interface-based JDK proxies or subclass-based proxies depending on configuration and target type.

The distinction can affect which methods are interceptable, behavior around final methods, visibility, concrete-type assumptions, and instanceof checks.

A proxied bean should be consumed through its externally visible contract rather than assuming that the runtime object is exactly the concrete class written in source.

8.6. Transaction Propagation

Comparison of REQUIRED and REQUIRES NEW behavior in Spring transaction propagation
Spring transaction propagation

Nested service calls can decide whether to join or create a transaction through propagation semantics.

The common REQUIRED behavior is:

transaction exists -> join it
no transaction     -> create one

REQUIRES_NEW can suspend the outer transaction and request an independent one:

outer tx
  ↓ suspend
inner new tx
  ↓ commit/rollback
outer tx resumes

REQUIRES_NEW is not “safer.” It can require another connection, commit independently, and remain committed even when the outer transaction later rolls back.

NESTED is conceptually different and can rely on savepoint behavior supported by the selected transaction manager and underlying resource.

An annotation alone does not prove that declarative transaction management has started a transaction; the call must also cross the appropriate proxy boundary. A method invoking another method on the same object may bypass interception, so the expected propagation rule need not apply. Verification should use a controlled failure after one of two writes and confirm whether both database changes are rolled back. A database transaction and a remote HTTP call are not one atomic unit; external side effects require a separate idempotency or compensation contract.

8.7. Isolation and readOnly

Propagation and isolation solve different problems.

Isolation asks what concurrent transactions may observe and with which guarantees.

readOnly = true can be a provider/database hint or optimization and is not, by itself, an authorization boundary. If writes must be impossible, database privileges and actual DBMS behavior must enforce that requirement.

8.8. Rollback Semantics

Spring's default rollback rules do not treat every checked and unchecked exception identically.

If a business failure is represented by an exception whose type changes rollback behavior, that choice becomes part of the transaction contract.

Catching and swallowing a runtime exception inside a transaction can also produce surprising results, including continuation of invalid logic or an already-marked rollback-only transaction.

Error handling and commit policy must therefore be designed together.

8.9. Thread Boundaries Are Transaction Boundaries

A transaction context that relies on thread-bound state should not be assumed to move automatically to another thread.

@Transactional method
  ↓
async task / another thread

does not imply that the asynchronous work participates in the original transaction.

Reactive context propagation, servlet threads, and virtual threads use different execution/context models. Transaction behavior must be validated against the actual programming model.

8.10. Configuration Precedence and Ownership

The key server.port defined in five property sources where the command-line value 9090 shadows the others and the next source becomes visible when a higher one is removed
Spring Boot property source precedence

Spring Boot composes external configuration from multiple sources: application files, profiles, environment, system properties, command-line arguments, and defaults.

The engineering question is not merely which value wins; it is who owns the value and when it is allowed to change.

@ConfigurationProperties is useful for grouping related settings into a typed, validated contract.

8.11. Auto-configuration and Ownership Transfer

Boot auto-configuration is conditional rather than magical.

Conditions can depend on classpath, properties, existing beans, and application type. When application code defines its own bean, some auto-configuration deliberately backs away.

Startup diagnosis should therefore ask:

which condition matched?
which bean already existed?
which property changed behavior?

not merely read the final exception.

8.12. Test Slices, Context Caching, and Test Cost

Test slices such as @WebMvcTest and @DataJpaTest communicate which part of the Spring system a test intends to verify.

Making every test a @SpringBootTest can increase startup cost, bring unrelated beans into failures, and enlarge the diagnostic surface.

Spring's test infrastructure can cache compatible application contexts. Tests that unnecessarily dirty the context or create unique dynamic configuration reduce this benefit.

A useful target is:

smallest realistic test scope
+
real integration at critical boundaries

8.13. Cache Abstraction Does Not Define Consistency Policy

Cache-aside stale fill after invalidation with computed steps and state transitions
Cache-aside stale fill after invalidation

@Cacheable makes result caching convenient, but it does not answer:

which dimensions belong in the key?
when is the value stale?
how is it invalidated after writes?
is authorization scope part of the key?
how do multiple instances converge?
how is cache stampede bounded?

Provider choice solves only part of the problem.

Caching should be treated as a read-correctness and accepted-staleness contract, not merely as a performance annotation.

Spring Boot 4.1.1 is the current stable line, but framework behavior remains version-specific. Historical Spring material can teach durable concepts; current APIs and supported Java ranges should always be verified against the exact Spring Boot and Spring Framework documentation in use.

8.14. The Failure Window Between Transaction Commit and External Effects

A database update protected by @Transactional and a subsequent remote HTTP call or message publication do not constitute one atomic operation. The database may commit and the process may stop before publishing; conversely, a message may be emitted before the database transaction rolls back. Retries alone do not close this gap because an external side effect can be applied twice. The application must state its actual consistency contract.

Writing an outbox record alongside business changes in the same local transaction makes the database state and publication intent atomic. A separate publisher later sends pending records. If it crashes after delivery but before marking the event as sent, it may deliver the same event again. The consumer therefore needs event-identity-based idempotency or equivalent deduplication. The outbox pattern by itself does not establish end-to-end exactly-once execution.

In Spring, transactional interception depends on calls passing through the configured proxy. Direct self-invocation of a @Transactional method may bypass the advice that the design expects. Remote calls performed while holding an open database transaction can also extend connection and lock occupancy unnecessarily. Validation must inject failures before and after commit, during repeated publication, and during duplicate consumption. In environments where the database schema is immutable, an outbox table must not be silently assumed; the achievable reliability contract is constrained by the existing persistence model.

8.15. Request Context, Authorization, and Delayed Asynchronous Work

Authorization performed for an HTTP request is not a perpetual permission grant to every delayed task. User, resource and action must be validated at the boundary of each persistent mutation; cached context must not replace current permissions after a workspace or role change. Neither SecurityContext nor request-scoped data should be assumed to propagate automatically to newly created threads. Trust in a captured context and current authorization are different questions.

Spring's imperative transaction management is thread-bound by default: starting another thread does not automatically transfer that transaction. Reactive execution uses a different context-propagation model. Holding a database transaction open while waiting for an external service, a long-running media stream or user input unnecessarily increases connection occupancy.

When asynchronous work completes after the target version has changed, its result must be revalidated against the new context or discarded in a controlled manner. This prevents older completions from overwriting newer state.

8.16. Safe Defaults and Conflict Resolution in Workspace-Scoped APIs

User preferences such as shortcut bindings need a contract that separates preference key, workspace and command identity. An API returning bindings for one workspace should not unnecessarily expose commands belonging to inaccessible workspaces. Shared commands can be declared explicitly; a command that is unassigned by default must not execute without a binding. Resolving a conflict inside one workspace must not erase a valid binding from another.

API safety is not established by an HTTP 200 response. Requests that complete out of order across reads, updates and workspace changes; repeated saves; unauthorized command identifiers; and nonexistent target records require separate contract tests. A revision or request generation can distinguish persistent configuration from the client's current effective mapping.

References

  • Brendan Gregg. Systems Performance: Enterprise and the Cloud. Pearson Education, 2014.
  • Mor Harchol-Balter. Performance Modeling and Design of Computer Systems: Queueing Theory in Action. Cambridge University Press, 2013.
  • Oracle, Java SE Downloads and Release Information. https://www.oracle.com/java/technologies/downloads/
  • Spring Framework. Bean Scopes. https://docs.spring.io/spring-framework/reference/core/beans/factory-scopes.html
  • Spring Framework. Transaction Propagation. https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-propagation.html
  • Spring, GraalVM Native Images with Spring Boot. https://docs.spring.io/spring-boot/how-to/native-image/
  • Spring, Spring Boot JSON Support — Jackson 3. https://docs.spring.io/spring-boot/reference/features/json.html
  • Spring, Spring Boot Observability. https://docs.spring.io/spring-boot/reference/actuator/observability.html
  • Spring, Spring Boot Reference Documentation — 4.1.x. https://docs.spring.io/spring-boot/
  • Spring, Spring Boot System Requirements. https://docs.spring.io/spring-boot/system-requirements.html
  • Spring, Spring Boot Testcontainers and Service Connections. https://docs.spring.io/spring-boot/reference/testing/testcontainers.html
  • Spring, Spring Data JPA Reference Documentation. https://docs.spring.io/spring-data/jpa/reference/
  • Spring, Spring Framework Reference Documentation — 7.0.x. https://docs.spring.io/spring-framework/reference/
  • Spring, Spring Security Reference — Method Security. https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html
  • Spring. Spring Boot 4.1.1 System Requirements. https://docs.spring.io/spring-boot/system-requirements.html
  • IETF. RFC 9846, TLS 1.3, 2026. https://www.rfc-editor.org/info/rfc9846
Contents