jpa-patterns
JPA/Hibernate patterns for entity design, relationships, query optimization, transactions, auditing, indexing, pagination, and pooling in Spring Boot.
Best use case
jpa-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
JPA/Hibernate patterns for entity design, relationships, query optimization, transactions, auditing, indexing, pagination, and pooling in Spring Boot.
Teams using jpa-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/jpa-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How jpa-patterns Compares
| Feature / Agent | jpa-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
JPA/Hibernate patterns for entity design, relationships, query optimization, transactions, auditing, indexing, pagination, and pooling in Spring Boot.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# JPA/Hibernate Patterns
Use for persistence adapter implementation and performance tuning in Spring Boot.
> **DDD/Hexagonal context**: JPA entities in this skill live in `adapter/out/persistence/` — they are **persistence models**, not domain entities. Domain entities live in `domain/model/` and contain zero JPA annotations. A `MarketMapper` translates between the two. Never expose `MarketEntity` (JPA) outside the persistence adapter.
## When to Activate
- Designing JPA entities and table mappings
- Defining relationships (@OneToMany, @ManyToOne, @ManyToMany)
- Optimizing queries (N+1 prevention, fetch strategies, projections)
- Configuring transactions, auditing, or soft deletes
- Setting up pagination, sorting, or custom repository methods
- Tuning connection pooling (HikariCP) or second-level caching
- Debugging LazyInitializationException or slow query performance issues
## Entity Design
```java
@Entity
@Table(name = "markets", indexes = {
@Index(name = "idx_markets_slug", columnList = "slug", unique = true),
@Index(name = "idx_markets_status_ts", columnList = "status, created_at")
})
@EntityListeners(AuditingEntityListener.class)
public class MarketEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, unique = true, length = 120)
private String slug;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private MarketStatus status = MarketStatus.DRAFT;
@CreatedDate private Instant createdAt;
@LastModifiedDate private Instant updatedAt;
// Always initialize collection fields — prevents NullPointerException
@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PositionEntity> positions = new ArrayList<>();
}
```
Enable auditing:
```java
@Configuration
@EnableJpaAuditing
class JpaConfig {}
```
## Relationships
### @OneToMany / @ManyToOne (Bidirectional)
```java
// Parent side
@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PositionEntity> positions = new ArrayList<>();
// Convenience methods to keep both sides in sync
public void addPosition(PositionEntity pos) {
positions.add(pos);
pos.setMarket(this);
}
public void removePosition(PositionEntity pos) {
positions.remove(pos);
pos.setMarket(null);
}
// Child side
@ManyToOne(fetch = FetchType.LAZY) // Always LAZY on @ManyToOne
@JoinColumn(name = "market_id", nullable = false)
private MarketEntity market;
```
### @ManyToMany
```java
@Entity
public class UserEntity {
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<RoleEntity> roles = new HashSet<>();
}
// Use Set for @ManyToMany to avoid duplicate rows and better performance
```
### @OneToOne (Shared Primary Key)
```java
@Entity
public class UserProfileEntity {
@Id
private Long id; // Same as UserEntity.id
@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name = "id")
private UserEntity user;
}
```
## N+1 Prevention
```java
// N+1 BAD: separate query per market to load positions
List<MarketEntity> markets = repo.findAll();
markets.forEach(m -> m.getPositions().size()); // N additional queries!
// GOOD: fetch join in JPQL
@Query("select m from MarketEntity m left join fetch m.positions where m.status = :status")
List<MarketEntity> findActiveWithPositions(@Param("status") MarketStatus status);
// GOOD: @EntityGraph for selective loading
@EntityGraph(attributePaths = {"positions", "categories"})
List<MarketEntity> findByStatus(MarketStatus status);
// GOOD: DTO projection — no entity loading at all
@Query("""
select new com.example.MarketSummaryDto(m.id, m.name, m.status, count(p))
from MarketEntity m left join m.positions p
where m.status = :status
group by m.id, m.name, m.status
""")
List<MarketSummaryDto> findSummaries(@Param("status") MarketStatus status);
```
## Repository Patterns
### Spring Data Query Methods
```java
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
// Derived queries
Optional<MarketEntity> findBySlug(String slug);
List<MarketEntity> findByStatusOrderByCreatedAtDesc(MarketStatus status);
long countByStatus(MarketStatus status);
boolean existsBySlug(String slug);
// JPQL — prefer over native SQL for portability
@Query("select m from MarketEntity m where m.status = :status")
Page<MarketEntity> findByStatus(@Param("status") MarketStatus status, Pageable pageable);
// Bulk update — bypass entity loading
@Modifying
@Query("update MarketEntity m set m.status = :status where m.id in :ids")
int updateStatusForIds(@Param("status") MarketStatus status, @Param("ids") List<Long> ids);
}
```
### Interface-Based Projections
```java
// Column projections — only fetch what you need
public interface MarketSummary {
Long getId();
String getName();
MarketStatus getStatus();
Instant getCreatedAt();
}
Page<MarketSummary> findAllProjectedBy(Pageable pageable);
```
### Record-Based Projections (Java 16+)
```java
public record MarketDto(Long id, String name, MarketStatus status) {}
@Query("select new com.example.MarketDto(m.id, m.name, m.status) from MarketEntity m")
List<MarketDto> findAllAsDto();
```
## Transactions
```java
// Service layer: @Transactional on methods
@Service
@Transactional(readOnly = true) // Default readOnly for all methods
public class MarketQueryService {
public Page<MarketSummary> findAll(Pageable pageable) {
return repo.findAllProjectedBy(pageable);
}
@Transactional // Override for write methods
public Market updateStatus(Long id, MarketStatus newStatus) {
MarketEntity entity = repo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Market " + id));
entity.setStatus(newStatus); // Dirty checking — no explicit save() needed
return mapper.toDomain(entity);
}
@Transactional
public void deleteById(Long id) {
if (!repo.existsById(id)) throw new EntityNotFoundException("Market " + id);
repo.deleteById(id);
}
}
```
### Transaction Propagation
```java
// REQUIRED (default): join existing or create new
@Transactional(propagation = Propagation.REQUIRED)
// REQUIRES_NEW: always create new (independent transaction)
// Use for audit logging that must persist even if outer tx rolls back
@Transactional(propagation = Propagation.REQUIRES_NEW)
// NOT_SUPPORTED: suspend outer transaction
@Transactional(propagation = Propagation.NOT_SUPPORTED)
```
## Optimistic Locking
Prevent lost updates in concurrent writes:
```java
@Entity
public class MarketEntity {
@Version
private Long version; // Hibernate increments on every update
// Throws OptimisticLockException if stale version detected
}
```
## Soft Deletes
```java
@Entity
@SQLDelete(sql = "UPDATE markets SET deleted_at = now() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL") // Hibernate 6.3+; was @Where in older versions
public class MarketEntity {
@Column(name = "deleted_at")
private Instant deletedAt;
}
// repo.delete(entity) → sets deleted_at, doesn't physically delete
// repo.findAll() → automatically filters out deleted rows
```
## Pagination
```java
// Offset pagination
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
Page<MarketEntity> result = repo.findByStatus(MarketStatus.ACTIVE, page);
// Page response metadata
result.getTotalElements();
result.getTotalPages();
result.hasNext();
// Keyset/cursor pagination for deep pages (more efficient)
@Query("""
select m from MarketEntity m
where m.status = :status
and (m.createdAt < :cursor or (m.createdAt = :cursor and m.id < :lastId))
order by m.createdAt desc, m.id desc
""")
List<MarketEntity> findPage(
@Param("status") MarketStatus status,
@Param("cursor") Instant cursor,
@Param("lastId") Long lastId,
Pageable pageable
);
```
## Batch Operations
```java
// Batch insert
@Transactional
public void bulkCreate(List<MarketEntity> entities) {
repo.saveAll(entities); // With hibernate.jdbc.batch_size=50
}
// Efficient bulk update — no entity loading
@Transactional
public void deactivateExpired(Instant cutoff) {
int updated = repo.updateStatusForIds(MarketStatus.CLOSED, findExpiredIds(cutoff));
log.info("Deactivated {} expired markets", updated);
}
```
## Indexing and Performance
```java
// Composite index matching common query patterns
@Table(name = "markets", indexes = {
@Index(columnList = "status"), // filter by status
@Index(columnList = "status, created_at DESC"), // status + sort by date
@Index(columnList = "user_id, status"), // per-user filtered list
@Index(columnList = "slug", unique = true), // slug lookup
})
```
Config for N+1 detection in development:
```yaml
# application-dev.yml
spring.jpa.properties:
hibernate.generate_statistics: true
logging.level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
```
## Connection Pooling (HikariCP)
```yaml
spring.datasource.hikari:
maximum-pool-size: 20 # cpu_cores * 2 + disk_spindles (rule of thumb)
minimum-idle: 5
connection-timeout: 30000 # 30s — fail fast
idle-timeout: 600000 # 10m
max-lifetime: 1800000 # 30m — must be < DB timeout
validation-timeout: 5000
# PostgreSQL specific
data-source-properties:
applicationName: my-service
# PostgreSQL LOB handling
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: true
```
## Migrations
- Use Flyway or Liquibase; never rely on `hibernate.ddl-auto=create` in production
- Name migrations: `V{version}__{description}.sql` (e.g., `V002__add_market_slug_index.sql`)
- Migrations are additive — never rename/drop columns without a plan
- Test migrations in CI against a real database (Testcontainers)
```yaml
spring.flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true # For existing databases
```
## Testing Data Access
- Use `@DataJpaTest` with Testcontainers (`@ServiceConnection`)
- Assert N+1 with Hibernate statistics in tests
- Use `TestEntityManager.flush()` to flush before querying
**Remember**: JPA entities are persistence adapters — keep them separate from domain entities. Keep queries intentional, transactions short, and N+1 eliminated. Index for your actual read/write paths.
For domain entity modeling (Aggregates, Value Objects), see skill: `ddd-java`.Related Skills
zero-trust-patterns
Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.
webrtc-patterns
WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.
webhook-patterns
Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.
wasm-patterns
WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).
ux-micro-patterns
UX micro-patterns for every product state: Empty States, Loading States (skeleton screens, spinners, optimistic UI), Error States, Success States, Confirmation Dialogs, Onboarding Flows, and Progressive Disclosure. These patterns apply to every feature — done wrong, they're the biggest source of user confusion.
typescript-patterns
TypeScript patterns — type system best practices, strict mode, utility types, generics, discriminated unions, error handling with Result types, and module organization. Core patterns for production TypeScript.
typescript-patterns-advanced
Advanced TypeScript — mapped types, template literal types, conditional types, infer, type guards, decorators, async patterns, testing with Vitest/Jest, and performance. Extends typescript-patterns.
typescript-monorepo-patterns
TypeScript monorepo patterns with Turborepo + pnpm workspaces. Covers package structure, shared configs, task pipeline caching, build orchestration, and publishing strategy.
terraform-patterns
Infrastructure as Code with Terraform — project structure, remote state, modules, workspace strategy, AWS/GCP patterns, CI/CD integration, and security hardening. The standard for managing production infrastructure.
swiftui-patterns
SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.
swift-patterns
Core Swift patterns — value vs reference types, protocols, generics, optionals, Result, error handling, Codable, and module organization. Foundation for all Swift development.
swift-patterns-advanced
Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.