Skip to content

bluetape4k-dependencies 2.0.0 in Practice Part 2: Data Access and Processing Lifecycles

A 3D miniature workshop where a central BOM board branches into compatibility, data, messaging, and operational-safety stations
Queries, transactions, cursors, chunks, checkpoints, and input sources have different lifecycles.

An API does not become memory-safe or restartable merely because it returns a Flow or accepts a chunk size. Tenant selection, transaction entry, cursor closure, writes, and checkpoint advancement each need a precise boundary.

This post reads Exposed 2.0.0 and Graph 1.0.0, selected by bluetape4k-dependencies 2.0.0, through that lens.

BoundaryOwnerClosing condition
tenant mappingapplicationconfiguration change or application shutdown
JDBC/R2DBC transactionroute or service callblock success, failure, or cancellation
cursor-backed streamcaller transactionstream consumption and transaction end
batch leasecurrent workerrenewal, terminal transition, or lease loss
checkpointjob state storeafter the successful write commits
graph input sourcecode that opened itcompletion, parse failure, or cancellation

Treating one helper as the owner of every row invites fallback routing, duplicate writes, cursor leaks, and unbounded input.

Close Tenant Selection with Exact Matching

Section titled “Close Tenant Selection with Exact Matching”

The bluetape4k-exposed-ktor-tenant-jdbc and -r2dbc adapters pass the TenantId bound to the current ApplicationCall to an application resolver. The resolver must be a fast O(1) in-memory exact match and must not send an unknown tenant to a default database.

val databases: Map<TenantId, Database> = loadTenantDatabases()
post("/orders") {
call.exposedTenantJdbcTransaction(
databaseResolver = { tenantId ->
databases[tenantId] ?: error("Unknown tenant: $tenantId")
},
blockingDispatcher = jdbcDispatcher,
) {
OrderTable.insert { /* request values */ }
}
}

The caller owns the resolver, Database, and blocking dispatcher. The R2DBC adapter is coroutine-native but does not close the resolver or R2dbcDatabase. Missing tenant context fails before resolver invocation. The tenant transaction bridge is therefore a routing-safety boundary, not merely a convenience wrapper.

JDBC Query by Example supports closed interfaces, Kotlin data classes, and Java record projections while pushing sort, limit, page, count, and exists operations into SQL. The R2DBC repository adds coroutine-native immutable terminals and cold Flow execution without exposing Reactor types.

Cold means the query starts on collection. It does not promise that every downstream path has a small memory bound. JDBC’s cursor-backed stream() must be consumed inside the current caller-owned transaction. Returning it beyond the transaction loses the cursor lifetime.

Open SpEL projections, nested or unknown properties, and custom search shapes that change cardinality fail before SQL execution. Close projection and cardinality rules before adding expression power.

Reading and writing 100 items does not justify advancing a checkpoint before the writer transaction commits.

while (true) {
val chunk = reader.readAfter(checkpoint) ?: break
lease.assertOwnerAndVersion()
transaction {
writer.write(chunk)
}
lease.assertOwnerAndVersion()
checkpointStore.save(chunk.lastCommittedKey)
}

The implementation fences owner and version immediately before writes and checkpoints so a worker that lost its lease cannot continue external writes or state transitions. If the execution later becomes FAILED, it preserves the checkpoint after the last successfully committed chunk. Restarting with the same parameters resumes after that key.

A checkpoint means “the last commit that does not need replay,” not “the last item read.”

Graph 1.0.0 adds sequential GraphRecordFlowReader implementations for CSV, Jackson2/3 NDJSON, GraphML, and Okio, aligning source ownership, cancellation, safe parse failure, and bounded edge staging. Import state separately tracks checkpoint identity, phase, claims, atomic updates, retry, and cleanup.

The API shape and the backend execution property remain distinct:

  • CHUNKED_READ/CHUNKED_EXPORT says callers can use a chunk-shaped API.
  • BOUNDED_CHUNKED_READ/BOUNDED_CHUNKED_EXPORT says the backend proved source-bounded execution.

In 1.0.0, only TinkerGraph advertises the bounded capability after proving traversal boundedness. Synchronous fallbacks for AGE, Neo4j, Memgraph, and FalkorDB keep chunk APIs without promising a heap bound for the complete source. The bounded capability issue records that distinction.

  1. Implement tenant resolution as an exact-match map with no default-database fallback.
  2. Keep QBE inside supported projection and cardinality rules and test pre-query rejection.
  3. Consume cursor-backed results inside the transaction block.
  4. Check lease ownership immediately before writes and checkpoint changes.
  5. Advance checkpoints only to successfully committed keys.
  6. Check backend capabilities instead of inferring source boundedness from Flow or chunk APIs.

Comments

Leave a note or reaction with your GitHub account.