← Spring Boot®: applications, contracts and operations
06 / 10 · 60 MIN

Queries, concurrency and data access

Choose query contracts and control concurrent state; interpret versions, JDBC outcomes and JPA context without confusing memory with committed data.

Contract and unit of work

Spring Boot 4.1.1 manages Spring Data JPA and Commons 4.1.1 and Jakarta Persistence API 3.2. Examples use that combination. Inherited CRUD methods have their own transactional configuration; a declared query such as findByStatus does not automatically receive the same configuration merely by belonging to the interface.

In the example, the service boundary covers querying and DTO mapping. That makes the unit of work in which data is read explicit. A repository signature does not replace a consistency decision, and readOnly should not be interpreted as universal write protection.

Bound values and controlled structure

The query uses :status and the argument declares Param("status"). The name associates the value with its logical position without depending on Java argument order. In JDBC, placeholders and separate arguments serve the same purpose of keeping values out of SQL text construction.

That does not mean any structural fragment can become a bound value. Sorting and permitted fields need a controlled contract; do not freely concatenate client text. In the example, the application chooses id for ordering. Assessment distinguishes the status value from query structure and access decisions.

Total, continuation and volume

Page includes total metadata, which can require an additional count query. Slice reports continuation without requesting that same global total. If the consumer only moves through a list, requiring Page can add work that does not help that task. A change must preserve the consumer’s actual needs.

Pageable already includes sorting and a limit; do not add a second Sort parameter to the same method. Use a unique tie-breaker when the primary field admits ties and bound accepted volume. Order becomes defined, but pages requested at different times remain subject to inserts, removals and state changes.

Investigate observed cost

A report can load orders and then access customer for each, producing several queries. Define which associations the report actually uses and evaluate a specific fetch plan, for example EntityGraph. Measure resulting queries and cardinality; an annotation guarantees neither one SQL statement nor safe behavior for every combination with collection pagination.

If the data query takes 18 ms and count takes 940 ms, investigation points to another requirement: the global total. Removing that requirement when the consumer does not use it can matter more than increasing connections. Diagnosis should follow observed costs without promising global improvement merely from a return-type change.

Version and a new decision

Version lets the provider detect that an entity changed since the revision read. An optimistic conflict prevents silently replacing an intervening committed change and marks the transaction for rollback. Checking may occur at flush or commit; calling save or changing a field does not establish final commit.

Imagine a reservation that saw capacity 8 while another transaction reduced it to 2. Repeating the old decision to reserve 5 after copying the new version bypasses protection. End the failed attempt, reread in a new unit of work, and reapply rules. Policy may reject or allow a bounded retry, but must consider current data.

Database and context can diverge

A bulk update Query uses Modifying but does not automatically update every already loaded entity. If Shipment still shows the old status after the bulk change reaches the database, that does not prove query failure. Compare affected-row count and context state before concluding.

clearAutomatically can clear the context but also discard unflushed changes. In the illustrative case, Shipment has no pending changes while a separate Note has unsaved text. Flushing the note before clearing and rereading preserves that intent. Review order against the entities actually changed; this is not a recipe for indiscriminately flushing stale state.

Read and update without invented guarantees

In the example, ready returns a Slice of DTOs with id, version and status. The limit is 20 and ordering uses id; there is no snapshot promise across requests. pack finds a managed entity and changes only READY to PACKED. The change is synchronized within the transaction, and a concurrent version can prevent commit.

Compare the preceding lesson’s conditional UPDATE: zero rows means neither accepted reservation nor proof of an existing SKU with zero capacity. The item may be absent or capacity insufficient. In both models, a technical result requires functional interpretation before success is announced, and retries require an explicit policy.

// Separate files in package com.acme.shipping, included in JPA and component scanning.
// Boot 4.1.1 managed Spring Data JPA 4.1.1 and Jakarta Persistence 3.2.
// Requires a configured JPA transaction manager, schema and database.
// Shipment.java
package com.acme.shipping;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Version;

@Entity
@Table(name = "shipment")
public class Shipment {
  @Id private Long id;
  @Version private long version;
  private String status;
  protected Shipment() {}
  public Long getId() { return id; }
  public long getVersion() { return version; }
  public String getStatus() { return status; }
  public void markPacked() {
    if (!"READY".equals(status)) throw new IllegalStateException("Shipment is not ready");
    status = "PACKED";
  }
}

// ShipmentRepository.java
package com.acme.shipping;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
  @Query("select s from Shipment s where s.status = :status")
  Slice<Shipment> findStatus(@Param("status") String status, Pageable page);
}

// ShipmentService.java
package com.acme.shipping;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ShipmentService {
  public record ShipmentView(Long id, long version, String status) {}
  private final ShipmentRepository repository;
  public ShipmentService(ShipmentRepository repository) { this.repository = repository; }

  @Transactional(readOnly = true)
  public Slice<ShipmentView> ready(int page) {
    if (page < 0) throw new IllegalArgumentException("Negative page");
    return repository.findStatus("READY", PageRequest.of(page, 20, Sort.by("id")))
        .map(s -> new ShipmentView(s.getId(), s.getVersion(), s.getStatus()));
  }

  @Transactional
  public void pack(Long id) {
    Shipment shipment = repository.findById(id)
        .orElseThrow(() -> new IllegalArgumentException("Unknown shipment"));
    shipment.markPacked();
  }
}
IN PRACTICE

ready requests up to 20 results per page and continuation, ordered by id. pack changes a managed entity inside the transaction; synchronization and version checking may occur at flush/commit. There is no version setter or snapshot promise across pages.

This separate example has one eligible JPA manager for unnamed annotations.

Common pitfalls

Concatenating values into JPQL; requesting Page when only continuation is needed; assuming sorting creates a snapshot; writing versions manually; retrying within a failed transaction; clearing the context without preserving pending changes.

Related topics: Persistence and transaction boundaries · HTTP routing and request contracts · Health, observability and diagnosis

Take this idea with you

A data-access outcome includes cardinality, cost, version and unit-of-work state. Those elements must be reconciled before declaring success or retrying a write.

Create account

Reference: Boot managed persistence dependencies · Spring Boot 4.1.1 / Spring Framework 7.0.9 / Java 25