jpa-patterns

JPA/Hibernate patterns for entity design, relationships, query optimization, transactions, auditing, indexing, pagination, and pooling in Spring Boot.

8 stars

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

$curl -o ~/.claude/skills/jpa-patterns/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/jpa-patterns/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/jpa-patterns/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How jpa-patterns Compares

Feature / Agentjpa-patternsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/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

8
from marvinrichter/clarc

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

8
from marvinrichter/clarc

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

8
from marvinrichter/clarc

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

8
from marvinrichter/clarc

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

8
from marvinrichter/clarc

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

8
from marvinrichter/clarc

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

8
from marvinrichter/clarc

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

8
from marvinrichter/clarc

TypeScript monorepo patterns with Turborepo + pnpm workspaces. Covers package structure, shared configs, task pipeline caching, build orchestration, and publishing strategy.

terraform-patterns

8
from marvinrichter/clarc

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

8
from marvinrichter/clarc

SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.

swift-patterns

8
from marvinrichter/clarc

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

8
from marvinrichter/clarc

Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.