
java-jpa-transaction-patterns
by HZeroxium
SKILL.md
name: java-jpa-transaction-patterns description: Transaction boundary and JPA/Hibernate usage patterns to prevent data bugs and performance issues (lazy loading pitfalls, N+1, locking). Includes checklists, code patterns, and regression tests. license: CC-BY-4.0 compatibility: jdk: "17+" persistence: ["Jakarta Persistence (JPA)", "Hibernate ORM"] runtime: ["Spring", "Jakarta EE", "Quarkus", "Micronaut", "Plain Java"] metadata: owner: "backend-platform" version: "1.0" tags: ["java", "jpa", "hibernate", "transactions", "locking", "n+1", "lazy-loading"]
java-jpa-transaction-patterns
Intent
Standardize how transactions and persistence contexts are used so that:
- correctness is preserved under concurrency,
- performance is predictable (no hidden N+1 / lazy traps),
- locking is explicit and safe.
This skill is framework-agnostic: it applies whether you use Spring @Transactional, JTA, or programmatic EntityTransaction.
Scope
In scope
- Transaction demarcation: where and how to start/commit/rollback
- Persistence context boundaries and flush behavior
- Lazy loading pitfalls and safe fetch patterns
- Locking strategies: optimistic vs pessimistic
- N+1 detection and elimination
- Regression tests for typical data bugs (lost updates, stale reads, deadlocks)
Out of scope
- Database-specific query tuning beyond JPA (see java-jdbc-performance for that)
- Complex CQRS/event-sourcing modeling decisions
When to use
- You see LazyInitializationException / “session is closed”
- Random data inconsistencies under concurrent updates
- Slow endpoints due to N+1 queries
- Deadlocks or lock timeouts
- “It works locally but fails under load” data-access issues
Key mental models
1) Transaction boundary defines correctness
A transaction is the unit of atomicity and isolation. If your business operation spans multiple SQL statements, the boundary must match the operation.
Rule of thumb:
- If multiple writes must succeed/fail together -> same transaction.
- If read-your-writes consistency matters -> same transaction.
- If you call external services -> avoid holding DB transaction open while waiting.
2) Persistence context is not “a cache you can ignore”
The EntityManager persistence context:
- tracks entity state (managed/detached),
- delays SQL until flush/commit,
- can hide extra queries via lazy loads.
If you don’t control the boundary, you don’t control SQL or locks.
3) Lazy loading is a feature, not a surprise
Lazy loading is safe only when:
- the persistence context is still open, AND
- you understand how many lazy associations will be accessed.
Default stance:
- avoid relying on lazy loads across layers
- decide fetch shape explicitly per use case
Standard architecture boundary
- Controller/Resource: request mapping + input validation only
- Service (application layer): transaction boundary and orchestration
- Repository/DAO: data access primitives (queries) only
Transaction boundary belongs in the Service layer by default.
Transaction patterns (with examples)
Pattern A — Single service-level transaction
Use when: typical CRUD or business operations requiring atomicity.
Spring-style:
- @Transactional on service method
Jakarta EE / plain JPA style:
- begin/commit around the service method
Guidelines:
- Keep transactions short
- Don’t do network calls inside transaction
- Prefer explicit exception mapping and rollback rules
Pattern B — Read-only transaction (still useful)
Use when: you need consistent reads and controlled fetch behavior.
Guidelines:
- Mark read-only where supported
- Avoid accidental flush (no entity mutation)
- Use projections/DTO queries for large graphs
Pattern C — Two-phase workflow: DB transaction + external call
Use when: you must call external services. Approach:
- Transaction 1: write “intent” and commit (e.g., outbox/event record)
- External call out of band
- Transaction 2: finalize state based on response
This reduces lock hold time and improves resilience.
Lazy loading pitfalls & solutions
Pitfall 1 — Open Session In View (OSIV) hides bugs
If the session remains open into the web layer, lazy loads happen “wherever,” producing unpredictable query counts.
Default recommendation:
- disable OSIV (or equivalent) for APIs
- shape data explicitly in repository layer
Pitfall 2 — Serialization triggers lazy loads
JSON serialization of entities can trigger lazy loads, causing N+1 and leaking internal relationships.
Solutions:
- never expose entities directly in API responses
- map to DTOs
- use explicit fetch joins or projections
N+1 query elimination toolkit (choose the smallest hammer)
Option 1 — DTO projection query (preferred for read APIs)
- Query only fields you need into a DTO
- Avoid loading entire entity graphs Benefits: minimal SQL, stable performance.
Option 2 — Fetch join for a known bounded relationship
- Use join fetch for specific association(s) Guardrail: join fetching multiple collections can explode result sets.
Option 3 — Batch fetching (ORM-level)
Use when: you need to traverse lazily, but want fewer round-trips.
- Hibernate batch fetching / @BatchSize reduces N+1 by grouping loads.
Option 4 — EntityGraph (JPA)
Use when: you need flexible per-use-case fetch plans without rewriting queries.
Locking: explicit decision-making
Default: optimistic locking with @Version
Use when: conflicts are rare.
- Adds version column
- Detects lost updates
- On conflict: retry or return 409 (Conflict) depending on API semantics
Pessimistic locking (SELECT ... FOR UPDATE semantics)
Use when: you must prevent concurrent modifications and conflicts are common. Risks:
- deadlocks under different lock ordering
- throughput reduction
Guidelines:
- keep locked section minimal
- lock in a consistent order (by primary key)
- set lock timeouts where supported
- prefer “skip locked” patterns only if business semantics allow
Flush/clear strategy (performance and correctness)
- Avoid calling flush too often; it increases lock time and IO
- In batch writes:
- flush and clear periodically to avoid memory bloat
- use JDBC batching if supported (see java-jdbc-performance)
Checklist: Transaction boundary
- Does the service method represent a single business operation?
- Are you holding a transaction while waiting on network IO? (avoid)
- Are exceptions mapped to rollback correctly?
- Do you rely on OSIV/lazy loads in controller/serialization? (avoid)
- Are write operations idempotent where required?
Checklist: N+1
- Enable SQL logging in test or use datasource proxy
- Write a regression test that asserts query count for hot endpoints
- Prefer DTO projections for list endpoints
- Use batch fetching or entity graphs for bounded traversals
Regression tests (minimum set)
- Lost update test:
- two concurrent transactions update same row -> version conflict expected (optimistic)
- Deadlock/lock timeout test (if you use pessimistic locks)
- Query count test (prevent N+1 regressions)
- Lazy initialization test:
- ensure service returns DTO and does not leak entity graph
Definition of Done (DoD)
- Transaction boundary resides at service layer (or justified otherwise)
- API responses are DTOs (no entity serialization)
- Locking strategy documented per write-critical workflow
- N+1 risk assessed and mitigated (projection/fetch/batch/entity graph)
- Regression tests added for bug class being fixed
Guardrails (What NOT to do)
- Do not expose JPA entities directly in API responses
- Do not rely on OSIV to “make lazy loading work”
- Do not add pessimistic locks by default without measuring deadlock risk
- Do not keep transactions open across network calls
Cursor usage (recommended)
Minimal context:
- Entities + repositories for the feature
- Service methods that orchestrate writes
- SQL logs / query plan hints (if available) Prompt snippet: “Use java-jpa-transaction-patterns. Identify transaction boundaries and N+1 risks. Propose DTO/projection or fetch plan. Recommend locking mode and add regression tests.”
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です