Skip to content

Bluetape4k Graph Part 2: Core API and Execution Models

Robotic builders assembling GraphOperations, Schema DSL, transaction, and coroutine execution model blocks on a 3D workbench
The point of a common API is to reduce repetition, not to hide reality.

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.

Core API execution flow from domain service to GraphOperations, Schema DSL, execution models, and graph storage adapter
Service code depends on graph concepts; execution and storage selection stay outside the domain flow.
AreaRepresentative methodsWhat service code gets
SessioncreateGraph, dropGraph, graphExistsgraph lifecycle for fixtures or tenant graphs
VertexcreateVertex, createVertices, findVertexById, updateVertex, deleteVertexnode CRUD without direct driver code
EdgecreateEdge, createEdges, findEdgesByLabel, findEdgesByStartId, findEdgesByEndIdrelationship CRUD and evidence lookup
Traversalneighbors, shortestPath, allPaths, aStarPathshared identity, recommendations, and path queries
AlgorithmpageRank, degreeCentrality, connectedComponents, bfs, dfs, detectCyclesranking, 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 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.

Transaction and batch write sequence for GraphOperations
Transaction blocks expose CRUD work and leave lifecycle commands to storage-specific code.

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.

ModelGood fit
SyncSingle operations, tests, ordinary blocking drivers
Virtual ThreadsBlocking drivers under higher concurrency
CoroutineKtor or coroutine-native service composition
API model benchmark chart for BFS latency with sync, virtual thread, and coroutine models
Single in-memory BFS favors sync; launch/create overhead favors coroutines. The workload decides.

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.

Comments

Leave a note or reaction with your GitHub account.