-
Notifications
You must be signed in to change notification settings - Fork 0
ADR 0004 Hibernate JPA
Tiana_ edited this page May 30, 2026
·
1 revision
Status: Accepted Date: 2026-04-25 Decider: Maintainer
The Spring Boot ecosystem offers two database access stacks:
- JPA / Hibernate (blocking, mature) - through Spring Data JPA. Industry standard since 2010, vast knowledge base, deep Hibernate features (caching, lazy loading, JPQL, criteria API, optimistic locking).
- R2DBC (reactive, newer) - non-blocking via Project Reactor. Better for high-concurrency workloads where threads are precious. Less mature, no JPA equivalent, no second-level cache, fewer features.
Java 21's virtual threads (Project Loom) change the calculus: blocking JDBC on virtual threads achieves most of the concurrency benefits R2DBC provides, without the cognitive overhead of reactive programming.
For FinCore's modular monolith with virtual threads enabled (spring.threads.virtual.enabled=true), JPA on virtual threads is competitive with R2DBC for our target throughput (1000-3000 tx/sec).
Use Hibernate JPA (Spring Data JPA) for v0.1 with the following settings:
- Spring Boot 3.5 LTS (Hibernate ORM 6.6.x)
- Virtual threads enabled
- HikariCP connection pool (separate read/write pools where workload differs)
- Optimistic locking via
@Versionon every entity -
@Enumerated(EnumType.STRING)for all enums (never ORDINAL) - Native queries for performance-critical paths only
-
@EntityGraphor explicitJOIN FETCHto prevent N+1 - Liquibase manages schema; Hibernate set to
validatein production (no auto-DDL)
Reconsider R2DBC if:
- p99 latency budget breaks at projected scale
- Specific bottleneck where blocking I/O dominates and virtual threads can't keep up
- Frontend-driven streaming use case where backpressure matters
- Massive ecosystem: every Spring Boot tutorial, Stack Overflow answer, library applies
-
MapStruct integration: KSP-based mapper generation works perfectly with
@Entityclasses -
Hibernate features available: second-level cache (Caffeine, Redis),
@EntityGraph, dirty checking, lazy loading -
Testcontainers integration:
@SpringBootTestwith real Postgres just works -
Optimistic locking:
@Versionannotation handles concurrent update conflicts cleanly - Migration ergonomics: Liquibase XML changelogs straightforward; entity mapping is by-the-book
- Onboarding: JPA expertise is common across the JVM ecosystem
-
Pre-virtual-thread era surprises: blocking I/O on platform threads = thread pool exhaustion under load. Mitigated by
spring.threads.virtual.enabled=trueand proper HikariCP sizing -
Entity != domain: JPA entities can't be
data class(Hibernate proxy issues). Domain layer uses different types (pure Kotlin), mapped via MapStruct. Adds one layer of indirection per service. -
Lazy loading footguns:
LazyInitializationExceptionoutside transactional context. Mitigated by@EntityGraphor explicit fetching, plus tests that exercise post-transaction reads. -
N+1 queries: classic Hibernate trap. Mitigated by Hibernate Statistics MBean enabled in dev,
@EntityGraph, code review checklist.
- R2DBC adoption in fintech is non-trivial - a respectable fraction of large banks run on Spring + JPA + virtual threads in 2026. We're not on the bleeding edge by sticking with JPA.
- Rejected for v0.1: maturity gap, cognitive overhead, no second-level cache
- Rejected: Hibernate Reactive exists but adds another stack to maintain
- Reconsider for specific high-concurrency services in v1.x
- Rejected: less integration with Spring ecosystem
- Rejected: smaller community, fewer enterprise contributors
- Adopters who prefer Exposed can wrap our
LedgerStorageProviderinterface
- Rejected: licensing model (paid for commercial Postgres support)
- Rejected: less integration with Spring Data
- Genuinely strong for complex queries - may use selectively for reporting (Y1 H2)
- Rejected: too much boilerplate at our scope
- Useful for small services; revisit if Hibernate proves overkill for some context
- Rejected: smaller ecosystem
- Compile-time SQL generation is appealing but switches us off Spring core
- Performance test: 1000 concurrent virtual threads posting transactions, measure p99 < 300ms
- N+1 detection in CI: Hibernate Statistics fail-on-N+1 enabled in tests
- Liquibase migration validates against running schema before app starts (
validatemode)
- Architecture-Resilience#7-connection-pool - HikariCP sizing
- Code-Rules - Hibernate-specific dos and don'ts
- Data-Model - schema reflecting JPA mapping
- Overview
- Services
- Data Model
- Domain Model
- Event Flow
- Security
- Observability
- Resilience
- SLA / SLI / SLO