Bluetape4k Graph Part 2: Core API and Execution Models

Part 1 covered graph storage selection. This part looks at graph-core, the common layer I wanted before the adapters
grew too much service-specific code. The goal is simple: service code should speak in vertices, edges, traversal, and
algorithms while graph storage adapters handle driver and query details. Without that boundary, every service slowly
grows its own tiny graph framework. Most of those frameworks are loved only on the day they are born.

| Area | Representative methods | What service code gets |
|---|---|---|
| Session | createGraph, dropGraph, graphExists | graph lifecycle for fixtures or tenant graphs |
| Vertex | createVertex, createVertices, findVertexById, updateVertex, deleteVertex | node CRUD without direct driver code |
| Edge | createEdge, createEdges, findEdgesByLabel, findEdgesByStartId, findEdgesByEndId | relationship CRUD and evidence lookup |
| Traversal | neighbors, shortestPath, allPaths, aStarPath | shared identity, recommendations, and path queries |
| Algorithm | pageRank, degreeCentrality, connectedComponents, bfs, dfs, detectCycles | ranking, components, traversal visits, and cycle checks |
val people = ops.createVertices( "Person", listOf( mapOf("email" to "alice@example.com", "name" to "Alice"), mapOf("email" to "bob@example.com", "name" to "Bob"), ))
ops.createEdges( "KNOWS", listOf(BatchEdge(people[0].id, people[1].id, mapOf("since" to 2026))))The default batch methods can loop over single-row APIs so every adapter can provide a baseline. Production adapters can override them with native batch writes and all-or-fail semantics. The common API should not force slow behavior; it should provide the shared contract and let storage-specific implementations do the efficient thing when they can.
Schema, transaction, and merge
Section titled “Schema, transaction, and merge”Schema is declared with VertexLabel and EdgeLabel, similar to Exposed table objects.
object PersonLabel : VertexLabel("Person") { val email = string("email") val name = string("name")}Schema management is not equally portable across graph storage engines. schemaManager() opens only when the selected
adapter can actually create the requested indexes or constraints. If a storage engine cannot safely provide that DDL,
the library fails with UnsupportedOperationException instead of returning success for a no-op. A missing unique
constraint should fail during setup or tests, not after production traffic has trusted it. A green setup log with no
actual constraint behind it is the kind of surprise that gets expensive quickly.

Merge follows the same rule. mergeVertex requires non-empty match properties and refuses to overwrite identity keys.
If the selected storage cannot provide atomic merge semantics, the library fails fast rather than pretending a
read-then-write fallback is safe under concurrency. “Usually safe” is not a comforting phrase when duplicate identity
vertices start showing up under concurrent traffic.
Execution models and benchmark shape
Section titled “Execution models and benchmark shape”| Model | Good fit |
|---|---|
| Sync | Single operations, tests, ordinary blocking drivers |
| Virtual Threads | Blocking drivers under higher concurrency |
| Coroutine | Ktor or coroutine-native service composition |

The ApiModelBenchmark smoke run used GraalVM JDK 25.0.3, JMH one fork, one warmup iteration, three measurement
iterations, one second per iteration, and -prof gc. PageRank throughput was 138,943.484 ops/s for sync, 40,283.460
ops/s for virtual threads, and 36,879.554 ops/s for coroutine Flow. BFS depth=5 latency was 4.724 us/op for sync,
18.668 us/op for virtual threads, and 20.244 us/op for coroutine Flow. These are short smoke results, so read the
Error column in the source before treating the ranking as release-grade evidence.
So coroutines are not the “always faster” model. Use them when Kotlin composition, cancellation, and structured concurrency matter. Use virtual threads when a Spring MVC service needs to run blocking graph drivers at higher concurrency. Execution model selection is not a preference poll; it follows the request path and the driver behavior.
Sources
Section titled “Sources”- GraphOperations
- GraphVertexRepository
- GraphTraversalRepository
- GraphAlgorithmRepository
- GraphMergeOperations
- GraphTransactionScope
- API model benchmark
Series
Section titled “Series”- Part 1: Database Selection Map
- Part 2: Core API and Execution Models
- Part 3: Graph I/O and Benchmarks
- Part 4: Workshop Scenarios and Service Integration
- Part 5: Reading the Virtual Threads Benchmark
Comments
Leave a note or reaction with your GitHub account.