Skip to content

ADR 0004 Hibernate JPA

Tiana_ edited this page May 30, 2026 · 1 revision

ADR-0004: Hibernate JPA over R2DBC for v0.1

Status: Accepted Date: 2026-04-25 Decider: Maintainer

Context

The Spring Boot ecosystem offers two database access stacks:

  1. 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).
  2. 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).

Decision

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 @Version on every entity
  • @Enumerated(EnumType.STRING) for all enums (never ORDINAL)
  • Native queries for performance-critical paths only
  • @EntityGraph or explicit JOIN FETCH to prevent N+1
  • Liquibase manages schema; Hibernate set to validate in 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

Consequences

Positive

  • Massive ecosystem: every Spring Boot tutorial, Stack Overflow answer, library applies
  • MapStruct integration: KSP-based mapper generation works perfectly with @Entity classes
  • Hibernate features available: second-level cache (Caffeine, Redis), @EntityGraph, dirty checking, lazy loading
  • Testcontainers integration: @SpringBootTest with real Postgres just works
  • Optimistic locking: @Version annotation 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

Negative

  • Pre-virtual-thread era surprises: blocking I/O on platform threads = thread pool exhaustion under load. Mitigated by spring.threads.virtual.enabled=true and 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: LazyInitializationException outside transactional context. Mitigated by @EntityGraph or 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.

Neutral

  • 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.

Alternatives considered

R2DBC with reactive stack

  • 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

Exposed (JetBrains' Kotlin SQL DSL)

  • Rejected: less integration with Spring ecosystem
  • Rejected: smaller community, fewer enterprise contributors
  • Adopters who prefer Exposed can wrap our LedgerStorageProvider interface

jOOQ (type-safe SQL builder)

  • 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)

Plain JDBC with Spring JdbcTemplate

  • Rejected: too much boilerplate at our scope
  • Useful for small services; revisit if Hibernate proves overkill for some context

Micronaut Data + JPA

  • Rejected: smaller ecosystem
  • Compile-time SQL generation is appealing but switches us off Spring core

Validation

  • 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 (validate mode)

Related

Clone this wiki locally