Spring Backend · Lesson 1

Production Spring Boot Backend Systems Explained

Build a production-minded Spring Boot order service by separating HTTP, application, data, security, configuration, operations, testing, and incident-recovery layers.

Quick answer

A production Spring Boot backend is not a controller connected directly to a repository. It is a set of deliberately separated boundaries that turns an HTTP request into a durable, authorized, observable business outcome. The separation matters because each boundary has a different failure mode: malformed input should not consume inventory capacity, authorization should not be inferred from a DTO, a transaction should not outlive its database work, and an internal failure should not become a stack trace sent to a customer.

For a practical order service, use eight production layers: the HTTP boundary, declared request validation, authentication and authorization, the application-service and transaction boundary, persistence and concurrency control, configuration and secrets, operations and observability, and verification plus incident recovery. They are layers of responsibility rather than eight network hops. A small application can implement several in one module while still keeping the decisions distinct.

Spring Boot 4.1 is the mainline used in this course. The examples use the familiar imperative Spring MVC model and Java 17 syntax. A Spring Boot 3.5 service can use the same architectural approach, but teams should confirm its exact Spring Framework, Spring Security, and Bean Validation integration before copying framework-specific configuration. Version numbers identify supported behavior; they do not replace release-note review or an upgrade test.

The goal is a backend that makes its promises explicit. A successful order response means the caller was permitted to place the order, the input was structurally acceptable, relevant business invariants were checked under the right concurrency model, the state transition committed, and the response can be explained from safe operational evidence. Returning 200 OK after an exception was swallowed is not production correctness.

Shared order-service incident stage

The shared incident begins during a flash sale. The order service accepts a POST /orders request for a scarce product. Initially the API appears healthy: pods respond, CPU is moderate, and the controller can deserialize JSON. Then concurrent requests pile up behind inventory updates. The inventory database connection pool shows increasing wait time before queries even start. Customers retry after slow responses, which raises the offered load and hides the original cause beneath timeout noise.

At the HTTP boundary, oversized request bodies and malformed JSON should be rejected before expensive business work. Declared validation should then reject an absent address or a negative quantity without claiming that a valid shape proves stock is available. Authentication and authorization must establish the customer and tenant before the service selects an order account. The application service owns the business command and transaction; it must not start a remote payment call inside a database transaction merely because all code is reachable from one controller.

For the incident, the useful timeline is: request admission and transport parsing; structural validation; identity and ownership checks; a short transaction that reserves inventory and records an idempotent order transition; an after-commit or durable-outbox handoff for external payment work; then response serialization and telemetry. When pool wait climbs, responders can first reduce nonessential traffic, bound admission, and inspect transaction duration. They should not increase pool size blindly, because more concurrent database work can worsen lock contention and make recovery slower.

The same timeline is compatible with the validation and exception articles in this path. Validation explains which checks occur before the transaction and which invariants must be protected inside it. Exception handling explains how a validation error, an ownership failure, an inventory conflict, an unavailable dependency, and a committed response take different error paths. Keeping one incident lets readers connect a framework annotation to an operational consequence.

Core mechanism and evidence boundary

The HTTP layer converts protocol data into an application command. Controllers should stay thin: bind path variables, headers, query parameters, and a request DTO; trigger declared validation; obtain the authenticated principal through the security context; call an application service; and select the HTTP response. A controller is a poor home for inventory rules, transaction orchestration, JPA entity graphs, or retry loops because it makes transport decisions and domain decisions impossible to test independently.

The application-service layer expresses the use case. PlaceOrder can decide that one open cart becomes one pending order, that an idempotency key refers to one previous outcome, and that an inventory reservation must be atomic with the order transition. The persistence layer then implements the concurrency contract, such as a conditional update, optimistic lock, or a database constraint. The database is not a passive storage detail: it is the final arbiter for uniqueness and concurrent stock decisions when multiple application instances race.

Evidence must follow those ownership lines. A metric may count order.command.accepted, validation failures by a bounded rule name, database pool wait, transaction duration, reservation conflicts, and safe status families. It must not use an order ID, email address, exception message, authorization header, or a raw request path as a metric label. Those values are sensitive, unbounded, or both. Correlation IDs belong in access-controlled logs and traces with a retention policy; sampled traces are useful explanations of individual paths, not a complete population measurement.

Configuration is another boundary. Bind reviewed, typed settings such as a maximum body size, database pool limits, payment endpoint, and feature flags outside business logic. Secrets should arrive through an approved secret delivery mechanism, not a committed application-prod.yml, a log line, or a diagnostics response. Profiles help choose environment-specific configuration, but profiles are not a secret store. Record a configuration version or deployment identifier with events so an incident can be correlated with a change without publishing secret values.

Production operation closes the loop. Health endpoints distinguish whether a process is alive from whether it is ready to accept routed traffic. Metrics show pressure and user outcomes, logs preserve safe diagnostic context, and traces follow selected requests across boundaries. An alert should lead to a runbook action backed by an invariant: for example, shed optional quote calculations before reducing the correctness checks on an inventory reservation. The evidence boundary prevents dashboards from becoming a second privacy incident.

Minimal reproducible Spring example

This Java 17 example shows a small HTTP-to-application boundary. It deliberately leaves inventory concurrency to the service and repository contract rather than pretending a controller annotation solves it. In a Spring Boot 4.1 application, the exact dependency coordinates and framework minor versions should come from the selected BOM; Spring Boot 3.5 teams should retain the same separation and verify their matching API documentation.

package com.example.orders;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import java.net.URI;
import java.util.UUID;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/orders")
public final class OrderController {
    private final PlaceOrderService placeOrderService;

    public OrderController(PlaceOrderService placeOrderService) {
        this.placeOrderService = placeOrderService;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> placeOrder(
            @AuthenticationPrincipal CustomerPrincipal customer,
            @RequestHeader("Idempotency-Key") @NotBlank String idempotencyKey,
            @Valid @RequestBody PlaceOrderRequest request) {
        PlacedOrder order = placeOrderService.place(new PlaceOrderCommand(
                customer.customerId(), idempotencyKey, request.sku(), request.quantity()));
        return ResponseEntity.created(URI.create("/orders/" + order.orderId()))
                .body(new OrderResponse(order.orderId(), order.status()));
    }

    public record PlaceOrderRequest(@NotBlank String sku, @NotNull @Positive Integer quantity) { }
    public record OrderResponse(UUID orderId, String status) { }
}

The @Valid marker activates declared constraints on the request object, but it does not authorize the customer, prove that the SKU exists, prove stock remains, or make the idempotency key unique. The authenticated principal is deliberately supplied separately from the JSON body. That avoids accepting a customer ID selected by the caller and makes ownership checks explicit in the service.

The service can use a transaction around the locally durable state change. If it must contact payment or shipping, use an intentional protocol such as an outbox and a compensating business state; a local @Transactional boundary does not atomically include a remote service. Returning 201 Created only means the order resource was accepted and recorded according to the service contract. It does not claim an external payment has settled unless the state model says so.

Failure modes and dangerous misconceptions

“A controller is the application.” A controller is only one adapter. When it contains authorization logic, JPA queries, payment retries, and response formatting, small changes create untestable combinations and security review becomes difficult. Keep protocol conversion narrow and make the use case callable from an HTTP test, a queue consumer, or a scheduled repair job without recreating an HTTP request.

@Valid proves the order is valid.” It proves only the declared constraints that were evaluated on the bound object. It cannot prove inventory, uniqueness, current account balance, ownership, or a race-sensitive invariant. A request can be well formed and still receive an authorization denial or conflict from the application and persistence layers.

“One transaction makes distributed work atomic.” A normal Spring transaction coordinates configured local transaction resources. It does not automatically extend across a new thread, a message broker, a payment provider, or an HTTP call. Holding a database transaction open while waiting on a remote provider also consumes connections during the flash-sale incident.

“More pool connections repair pool wait.” Pool wait is evidence of a mismatch between offered work and database capacity. Raising a client pool limit can move waiting into the database, make locks more contended, and harm unrelated queries. Measure query latency, lock waits, transaction duration, and sustainable concurrency before changing the limit.

“Healthy pods mean the order journey is healthy.” Liveness can be green while all useful requests wait on inventory. Readiness can be green while a downstream quota is exhausted. Measure the user-facing order outcome and tail latency alongside dependency pressure, and make probes cheap enough that they do not compete with the work they assess.

Security, privacy, transaction, capacity, and cost implications

Authenticate early enough to avoid expensive anonymous work and authorize the requested action against server-side ownership data. Do not treat an accountId in the body as authority. Limit request size, collection length, pagination, and decompression before allocating unbounded memory. Rate limits and concurrency controls are both capacity protections and security protections when accidental or abusive traffic targets a scarce dependency.

Minimize data flow between layers. A request DTO need not include internal cost, fraud, or operational flags; a response DTO need not expose database identifiers or exception text. Redact secrets and personal data before logs and error documents. A useful correlation identifier can be returned to a client, but it should be opaque, bounded, and useless as an authorization credential.

Transactions should be as short as the invariant requires. Inventory reservation and order persistence may need one atomic local unit; sending email, charging a remote card, and rendering a large response normally do not. Short transactions free connections sooner and reduce the lock footprint. They do not eliminate the need for idempotency, compensation, and reconciliation after a partial distributed workflow.

Cost decisions should be explicit. Extra database replicas, large pools, verbose tracing, and permanently high idle capacity each cost money and sometimes increase failure blast radius. Establish a load profile, define an acceptable checkout outcome, reserve headroom for failures, and retain telemetry at an intentional cardinality and duration. Capacity without an admission policy merely lets the service fail later at greater expense.

Testing and production validation

Test each layer at the boundary it owns. Controller tests should assert request binding, declared validation, principal extraction, status codes, and safe response shapes. Application-service tests should cover ownership, idempotency, state transitions, and invariant decisions. Persistence integration tests should exercise the actual database constraints and concurrent reservation behavior. A fast unit test that mocks a repository cannot establish that two real transactions will not oversell a SKU.

For the flash-sale incident, use a production-like load test with a controlled inventory hotspot. Measure accepted and rejected commands, successful orders, duplicate-idempotency outcomes, p95 and p99 latency, transaction duration, database pool wait, lock waits, and retry volume. Introduce slow inventory queries and a partial instance loss. Verify that the system sheds work or returns a documented retryable response before memory and queues become unbounded.

Before release, review configuration precedence, secret injection, migrations, timeouts, connection-pool settings, Actuator exposure, and rollback behavior. Deploy a canary with dashboards that distinguish offered requests, accepted work, conflicts, validation failures, authentication failures, and server failures. After a change, compare the same query and pool evidence rather than declaring success from a single synthetic request.

Operations checklist

  • Keep controllers responsible for HTTP translation, not domain orchestration.
  • Define the eight layers and the invariant owned by each one.
  • Validate structure at the request boundary; enforce business invariants in the application and persistence boundaries.
  • Authenticate and authorize with server-side identity and ownership data.
  • Keep local transactions short and do not wait on remote calls inside them.
  • Use database-enforced concurrency and uniqueness controls for shared state.
  • Bind typed configuration; deliver secrets separately and never emit them in diagnostics.
  • Bound request size, concurrency, queueing, retries, and telemetry cardinality.
  • Measure customer outcomes together with pool wait, transaction duration, and lock evidence.
  • Rehearse the flash-sale recovery path before an incident, then verify recovery with fresh evidence.

Official sources

Continue the learning path

Make the request boundary precise with Spring Boot Request Validation Explained and make failures safe with Spring Boot Exception Handling Explained. Continue through the Spring Backend course, browse adjacent Topics, and use Spring Backend Engineering to revisit the ordered cluster.

Knowledge check

Check your understanding

Answer both questions correctly to mark this lesson as mastered. You can retry without penalty.

1. Flash-sale latency and database pool wait rise together while CPU stays moderate; which response respects every boundary before changing capacity?

2. A client loses the response after the order transaction may have committed; which operational conclusion is supported by the available evidence?