Interactive architecture companion

JPA to Exposed: Make Transaction Ownership Visible

Exposed trades part of JPA/Hibernate's object-graph automation for explicit SQL intent. The decisive migration question is not which repository opens a transaction, but which application boundary owns the complete unit of work.

01

Invariant

First, change the mental model

JPA / Hibernate

JPA / Hibernate default

managed entity → dirty checking → flush

Strength: productive object graphs, mature ecosystem, rich persistence context.

Cost: I/O can hide behind proxies, N+1 traversal, flush timing, and entity lifecycle.

JetBrains Exposed

Exposed DSL / repository

explicit select / join / map → explicit insert / update / delete

Strength: SQL intent and execution points stay visible in application code.

Cost: query shape, mapping, writes, and transaction boundaries require explicit design.

02

ARCHITECTURE COMPARISON

Compare the architectures before tracing the calls

The first diagram aligns a typical Spring Data JPA and Hibernate stack with Exposed at the same responsibility level. The second preserves the repository's Exposed transaction-ownership diagram as the detailed view.

JPA/Hibernate and Exposed responsibility architecture Compare the transaction boundary, persistence state, repository role, and database transport at the same level.
JPA and Exposed architecture comparison: a typical Spring Data JPA and Hibernate path uses a transaction-bound EntityManager and persistence context over JDBC, while Exposed uses caller-owned JDBC or R2DBC transaction contexts with explicit repositories and SQL operations.
JPA/Hibernate and Exposed responsibility architecture
JPA and Exposed architecture comparison: a typical Spring Data JPA and Hibernate path uses a transaction-bound EntityManager and persistence context over JDBC, while Exposed uses caller-owned JDBC or R2DBC transaction contexts with explicit repositories and SQL operations.
Exposed JDBC and R2DBC transaction ownership Expand the Exposed side to see how JDBC and R2DBC carry the same caller-owned transaction rule.
Transaction ownership architecture: a service or controller boundary encloses JDBC transaction blocks and R2DBC suspend transaction blocks before repository calls reach the database.
Exposed JDBC and R2DBC transaction ownership
Transaction ownership architecture: a service or controller boundary encloses JDBC transaction blocks and R2DBC suspend transaction blocks before repository calls reach the database.
03

BOUNDARY LAB

Replay six boundary decisions

Select a path. The sequence, owner, terminal signal, code mapping, and risk statement update together.

04

CALL → RETURN → TERMINAL

Execution sequence

COMMIT

JDBC · one repository

One synchronous boundary encloses query, mapping, and commit.

Boundary owner
Controller / service transaction {}
Client
Controller / Service
JdbcRepository
Database
  1. 1 CALL Use case
  2. 2 CALL transaction { findAll() }
  3. 3 RETURN Mapped records
  4. 4 COMMIT Transaction boundary
COMMIT the block returns normally.
05

RESPONSIBILITY

Who owns what?

ActorOwnsReason
Application boundaryBegins and ends the business transactionKeeps multiple repository calls atomic
RepositoryExpresses queries, mapping, and writesDoes not secretly widen the use-case boundary
Exposed runtimeBinds transaction state to the JDBC thread or R2DBC coroutine contextMakes escaped work a correctness concern
DatabaseCommits or rolls back the unitSees one terminal outcome
06

MIGRATION MAP

From JPA habit to Exposed code

JPA habitExposed mapping
@Transactional service methodtransaction { ... } or suspendTransaction { ... } at the application boundary
Managed entity mutationExplicit update, insert, or delete repository operation
Lazy relationship traversalExplicit join or follow-up query inside the same boundary
Flush-time SQLSQL occurs where the Exposed DSL or repository call is made
Persistence-context lifetimeTransaction block / coroutine-context lifetime
07

DECISION GUIDE

Choose with the costs visible

DecisionSignal
Prefer JPA/HibernateComplex object graphs, domain navigation, and ecosystem integrations outweigh hidden I/O risk.
Prefer ExposedSQL shape, predictable I/O, Kotlin DSL composition, and explicit boundaries are primary.
Avoid a blind migrationMoving repository names without moving the mental model preserves boundary and N+1 mistakes.
Use both deliberatelyChoose per bounded context; do not mix transaction ownership inside one use case.
08

TRACEABLE CLAIMS

Source and test ledger

Why it matters Production source Test or executable example Verification
JDBC repository calls execute inside caller-owned transaction blocks. JdbcRepository ProductController rg -n "transaction \\{" examples/jdbc-demo/src/main/kotlin
The executable JDBC demo places the boundary at the controller use case. ProductController transaction-ownership.md rg -n "transaction \\{" examples/jdbc-demo/src/main/kotlin
R2DBC operations depend on the suspendTransaction coroutine context. R2dbcRepository MovieR2dbcRepositoryTest ./gradlew :bluetape4k-exposed-r2dbc:test
The R2DBC demo opens the suspending boundary before repository work. ProductController repository-patterns.md rg -n "suspendTransaction" examples/r2dbc-demo/src/main/kotlin
Repository tests exercise suspending query and transaction behavior. MovieR2dbcRepositoryTest coroutine-transactions.md ./gradlew :bluetape4k-exposed-r2dbc:test --tests "*MovieR2dbcRepositoryTest"
The manual makes coroutine lifetime and cold Flow collection explicit. coroutine-transactions.md repository-patterns.md ruby scripts/manual/validate_manuals.rb build/manual/module-inventory-1.11.0.json docs/manual/manifest.yaml
Selected scenario: