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.
Interactive architecture companion
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.
Invariant
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.
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.
ARCHITECTURE COMPARISON
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.
BOUNDARY LAB
Select a path. The sequence, owner, terminal signal, code mapping, and risk statement update together.
CALL → RETURN → TERMINAL
One synchronous boundary encloses query, mapping, and commit.
Controller / service transaction {}One service boundary composes calls so partial success cannot leak.
Service transaction {}The coroutine context carries the transaction through suspending repository work.
Coroutine suspendTransaction {}A cold Flow or channelFlow is collected before the transaction block exits.
Collector inside suspendTransaction {}Returning a cold Flow closes the transaction before collection begins.
Caller mistakenly outlives the transactionAn exception or coroutine cancellation must terminate the whole unit, not one repository call.
Transaction block / coroutine scopeRESPONSIBILITY
| Actor | Owns | Reason |
|---|---|---|
| Application boundary | Begins and ends the business transaction | Keeps multiple repository calls atomic |
| Repository | Expresses queries, mapping, and writes | Does not secretly widen the use-case boundary |
| Exposed runtime | Binds transaction state to the JDBC thread or R2DBC coroutine context | Makes escaped work a correctness concern |
| Database | Commits or rolls back the unit | Sees one terminal outcome |
MIGRATION MAP
| JPA habit | Exposed mapping |
|---|---|
@Transactional service method | transaction { ... } or suspendTransaction { ... } at the application boundary |
| Managed entity mutation | Explicit update, insert, or delete repository operation |
| Lazy relationship traversal | Explicit join or follow-up query inside the same boundary |
| Flush-time SQL | SQL occurs where the Exposed DSL or repository call is made |
| Persistence-context lifetime | Transaction block / coroutine-context lifetime |
DECISION GUIDE
| Decision | Signal |
|---|---|
| Prefer JPA/Hibernate | Complex object graphs, domain navigation, and ecosystem integrations outweigh hidden I/O risk. |
| Prefer Exposed | SQL shape, predictable I/O, Kotlin DSL composition, and explicit boundaries are primary. |
| Avoid a blind migration | Moving repository names without moving the mental model preserves boundary and N+1 mistakes. |
| Use both deliberately | Choose per bounded context; do not mix transaction ownership inside one use case. |
TRACEABLE CLAIMS
| 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 |