From spring
Design Spring Data repositories, derived queries, projections, auditing, paging, and scrolling across multiple Spring Data modules. Use this skill when designing Spring Data repositories, derived queries, projections, auditing, paging, scrolling, and repository-based persistence patterns that span multiple Spring Data modules.
npx claudepluginhub ririnto/sinon --plugin springThis skill uses the workspace's default tool permissions.
Use this skill when designing Spring Data repositories, derived queries, projections, auditing, paging, scrolling, null-safe repository contracts, and repository-based persistence patterns that span multiple Spring Data modules.
references/entity-callbacks-and-conversions.mdreferences/jpa-transactions.mdreferences/multimodule-repository-scanning.mdreferences/query-by-example.mdreferences/scrolling-patterns.mdreferences/spring-data-aot.mdreferences/spring-data-domain-events.mdreferences/spring-data-rest-exposure.mdreferences/store-specific-module-selection.mdMandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Use this skill when designing Spring Data repositories, derived queries, projections, auditing, paging, scrolling, null-safe repository contracts, and repository-based persistence patterns that span multiple Spring Data modules.
Use spring-data for repository abstraction, derived query methods, projections, auditing, paging, scrolling, Query by Example, custom repository extensions, and common Spring Data mapping patterns.
The ordinary Spring Data job is:
Pick the store-specific starter or module that matches the chosen persistence technology and pair it with the matching test support.
Choose one store module for the repository implementation path.
- JPA: spring-boot-starter-data-jpa
- JDBC: spring-boot-starter-data-jdbc
- R2DBC: spring-boot-starter-data-r2dbc
- MongoDB: spring-boot-starter-data-mongodb
- Redis: spring-boot-starter-data-redis
Keep spring-data itself focused on repository abstractions shared across those modules.
If the project already imports the Spring Data BOM for this release train, omit child Spring Data versions from dependency examples and keep the BOM import as the single version anchor.
The current stable Spring Data release train is 2025.1.5 with Commons 4.0.5. 2026.0.0-RC1 exists as the next train candidate, but it is not the default baseline until that train reaches GA.
interface CustomerRepository extends ListCrudRepository<Customer, Long> {
}
Optional<Customer> findByEmailIgnoreCase(String email);
record CustomerView(Long id, String email) {
}
Use Class<T> projection parameter when callers choose the projection shape at runtime:
record CustomerSummary(String email) {
}
<T> List<T> findByEmail(String email, Class<T> projectionType);
Callers pass the projection class:
List<CustomerView> views = repository.findByEmail("a@example.com", CustomerView.class);
List<CustomerSummary> summaries = repository.findByEmail("a@example.com", CustomerSummary.class);
The projection type must be a valid Spring Data projection interface or a DTO class such as a record. For class-based DTO projections, keep a single constructor or mark the constructor Spring Data should use with @PersistenceCreator. Dynamic projection works naturally with derived queries; declared queries must still return a shape that matches the selected projection.
Optional<Customer> findByEmailIgnoreCase(String email);
Spring Data Commons 4.0 and later align with JSpecify-style nullability. Use package-level @NullMarked as the default, then mark only the nullable exceptions explicitly:
| Annotation | Meaning | Typical use |
|---|---|---|
@NullMarked | types in the package are non-null by default | package-info.java |
@Nullable | this parameter or type usage may be null | optional parameters, nullable return types |
@NullUnmarked | temporarily opt one declaration out of @NullMarked | migration or mixed nullness areas |
@NullMarked
package com.example.customer;
import org.jspecify.annotations.NullMarked;
interface CustomerRepository extends ListCrudRepository<Customer, Long> {
Optional<Customer> findByEmail(@Nullable String email);
@Nullable
Customer findPrimaryContactByAccountId(Long accountId);
}
Use Optional<T> for absent aggregate results and @Nullable for truly nullable scalar or entity return types. Do not wrap Optional<T> itself in @Nullable; absence belongs inside the Optional.
Window<CustomerView> findFirst20ByAddressCityOrderByIdAsc(String city, ScrollPosition position);
@DataJpaTest
class CustomerRepositoryTests {
@Autowired
CustomerRepository repository;
@Test
void findsProjectionByEmail() {
List<CustomerView> views = repository.findByEmail("a@example.com", CustomerView.class);
assertThat(views).extracting(CustomerView::email).contains("a@example.com");
}
}
Optional, collections, slices, pages, or windows that match the real contract.If the project stays on the stable 2025.1.x line, keep examples aligned with that line. Open a release-train or migration reference before copying 2026.0.x RC behavior into a stable branch.
interface CustomerRepository extends ListCrudRepository<Customer, Long> {
Optional<Customer> findByEmailIgnoreCase(String email);
Slice<CustomerView> findByAddressCity(String city, Pageable pageable);
Window<CustomerView> findFirst20ByAddressCityOrderByIdAsc(String city, ScrollPosition position);
}
record CustomerView(Long id, String email) {
}
interface CustomerRepositoryCustom {
List<CustomerView> findRecentlyActiveCustomers(Instant since);
}
interface CustomerRepository extends ListCrudRepository<Customer, Long>, CustomerRepositoryCustom {
}
class PurchaseOrder {
@CreatedDate
Instant createdAt;
@LastModifiedDate
Instant updatedAt;
}
Enable the matching store-specific auditing configuration in the store-specific path rather than assuming JPA.
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnoreCase()
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING);
Example<Customer> probe = Example.of(new Customer("a@example.com", null), matcher);
Shared mapping callbacks and conversions belong to the common Spring Data model, but keep store-specific callback mechanics in the store-specific path.
findByEmailIgnoreCase
findByAddressCity
Use declared queries when derived query method names become unwieldy. The annotation name is shared across Spring Data modules, but the actual query language and advanced attributes stay store-specific.
@Query("...")
List<Customer> findActiveCustomers();
Keep common guidance at the level of 'declared query versus derived query'. Move JPQL, native SQL, SpEL, and store-specific @Query attributes to the store-specific path.
Spring Data reserves identifier-targeting base method names. When domain properties happen to collide with them, the behavior can surprise:
| Pattern | Behavior | Pitfall |
|---|---|---|
findById | maps to identifier equality | Id means the declared identifier property, not a random field named id |
existsById | checks identifier existence | does not derive a custom predicate from another id-like field |
deleteById | deletes by identifier | does not derive a custom delete predicate from another id-like field |
class Customer {
@Id Long pk;
Long id;
}
Optional<Customer> findById(Long id); // targets pk
Optional<Customer> findCustomerById(Long id); // targets property named id
Prefer explicit derived query names when a domain property could be confused with the identifier.
Choose the return type that matches what callers actually need:
| Type | Use when | Trims result |
|---|---|---|
Page | callers need total count and total pages | a separate count operation runs |
Slice | callers navigate forward through unknown total size | no count query; next-page existence is available |
Window | callers need scroll-based iteration and a window they can extract the next position from | can be offset- or keyset-backed |
// Page: caller needs total count
Page<CustomerView> findByAddressCity(String city, Pageable pageable);
// Slice: caller scrolls through unknown-length feed
Slice<CustomerView> findByAddressCity(String city, Pageable pageable);
// Window: caller scrolls with deterministic ordering and extracts the next position from the current window
Window<CustomerView> findFirst20ByAddressCityOrderByIdAsc(String city, ScrollPosition position);
Page is the heaviest because it runs a separate count query. Slice avoids the count but still uses pageable offset traversal. Window represents scroll-based iteration, and callers extract the next ScrollPosition from the current window with window.positionAt(...); the underlying scroll can be offset- or keyset-based depending on the repository method and store support.
Keyset scrolling constraints:
ScrollPosition must be carried forward from the previous Window result.OrderByIdAsc so every window position stays stable.Open references/scrolling-patterns.md when the blocker is WindowIterator, offset-versus-keyset ScrollPosition, or why a projection breaks keyset scrolling.
Enable auditing per store module. The entity annotations are shared, but the activation mechanism is store-specific and belongs in the store-specific path.
Auditing fields:
class PurchaseOrder {
@CreatedDate
Instant createdAt;
@LastModifiedDate
Instant updatedAt;
@CreatedBy
String createdBy;
@LastModifiedBy
String modifiedBy;
}
If using @CreatedBy or @LastModifiedBy, provide the auditor SPI that matches the store style: AuditorAware<T> for imperative repositories and ReactiveAuditorAware<T> for reactive infrastructure.
Open references/jpa-transactions.md when the blocker is declared-query transaction behavior, @Modifying, or a facade-level transaction boundary in a JPA store.
@CreatedDate
Instant createdAt;
WindowIterator, or projection constraints in keyset scrolling.@Query methods, or @Modifying behavior.