Skip to content

Bluetape4k Graph Part 4: Workshop Scenarios and Service Integration

Robotic builders reviewing abuse detection, recommendation, knowledge graph, and social network graph boards on a 3D workbench
Examples are not decoration. They are small laboratories for design choices.

This part maps the working bluetape4k-workshop graph examples. It is an overview, not a deep guide. The goal is to help you recognize whether your own service has the same shape of problem. If an article only lists example names and sends the reader back to the source tree, that is not documentation; it is homework distribution. So each example below explains the relationship model and the decision it helps validate.

ExampleGraph patternTakeaway
Abuser detectionUser-to-identifier identity graphShared device/IP/phone/payment/referral traversal
RecommendationPurchase graph and follow graphCo-buyer and friends-of-friends candidates
Knowledge graphDocument, entity, concept relationshipsSemantic relationships and hop traversal
Social networkPerson/company and multi-hop relationshipsFOAF, mutual connections, shortest paths

Abuser detection is one of the most natural fits for a graph. Start from one user, follow identifiers such as device, IP address, phone hash, payment token, or referral, then find other users sharing those identifiers. You can build this with relational joins, but the query grows awkward as identifier types multiply.

Abuser detection identity flow from seed user to shared identifiers, related users, suspicion evidence, and risk ranking
Shared identifiers turn abuse investigation into a readable graph traversal.

AbuserDetectionService.findAbuseCluster() starts from a seed user, follows outgoing identifier edges, then traverses back from those identifiers to related users. It excludes the seed user from the result. explainSuspicion() collects edge paths for evidence, detectReferralLoops() uses cycle detection, and rankSuspiciousUsers() uses PageRank for the review queue. Phone values should be hashes, and payment identifiers should be PCI-safe tokens.

Abuser detection entity graph with User, Device, IpAddress, PhoneNumber, PaymentMethod, and referral user relationships
The important modeling choice is to keep identifiers as vertices. Shared identifiers become the cluster signal.

The recommendation example covers both product recommendations and follow recommendations.

Recommendation example flow for co-buyer product candidates and friends-of-friends follow candidates
Separate candidate generation from scoring so the traversal is easy to read and test.

RecommendationService.recommendProducts() finds products bought by the seed user, finds co-buyers for those products, then recommends other products bought by those co-buyers. Already purchased products are excluded, and candidates are scored by distinct co-buyer count. recommendFollows() creates 2-hop follow candidates, excludes the seed user and existing follows, then sorts by mutual follow count. This is not a complete recommendation engine. It is a small skeleton for separating candidate generation from scoring. For large graphs, replace this explicit traversal shape with native Cypher or Gremlin queries. The common API makes the starting point and tests simpler; it should not wrap a heavy query until the bottleneck looks prettier.

Recommendation entity graph with User, Product, followee user, and scored candidates
PURCHASED builds product candidates; FOLLOWS builds follow candidates. Candidate generation and scoring stay separate.

The knowledge graph example models Document, Entity, and Concept vertices. MENTIONS connects documents to entities, RELATED_TO connects entities to other entities, and IS_A connects entities to concepts. The useful service operations go beyond “search”: findMentionedEntities(documentId), findRelatedEntities(entityId, depth), findConceptsForEntity(entityId), and inferRelationshipPaths(from, to) explain why two things are connected.

Knowledge graph entity graph with Document, Entity, Concept, and related entity relationships
Search finds candidate documents; graph paths explain why entities are connected.

This fits search and RAG systems where full-text or vector search finds candidate documents, while graph paths explain semantic relationships. It does not mean every search problem should become a graph problem. Full-text search and vector indexes are still the right tools for document retrieval; graph paths help when relationships and explanations matter.

The social network example keeps the classic graph problems concrete: Person and Company vertices, with KNOWS, FOLLOWS, and WORKS_AT edges. It demonstrates direct connections, N-degree traversal, friends-of-friends candidates, colleagues, shortest paths, all paths, and mutual connections. The example looks simple, but production code quickly adds the unglamorous rules: exclude the seed user, exclude existing connections, cap depth, and decide whether a relationship is directional.

Social network entity graph with Person, Company, KNOWS, FOLLOWS, and WORKS_AT relationships
Direction and depth limits decide whether a path is a recommendation or an explanation.

The important details are direction, depth, and exclusion rules. FOLLOWS is directional, KNOWS may be treated as bidirectional by service policy, and recommendation queries should exclude the seed user plus existing connections.

Spring Boot 4 users get GraphOperations, GraphSuspendOperations, and GraphVirtualThreadOperations beans from graph-spring-boot. TinkerGraph is the default when bluetape4k.graph.backend is missing, which makes local tests and small examples cheap.

bluetape4k:
graph:
backend: tinkergraph

Service code then uses normal bean injection.

@Service
class AbuseReviewService(
private val graph: GraphOperations,
) {
fun cluster(seedUserId: GraphElementId): AbuseCluster {
val service = AbuserDetectionService(graph)
return service.findAbuseCluster(seedUserId)
}
}

For Neo4j, keep the service code and change the storage properties.

bluetape4k:
graph:
backend: neo4j
neo4j:
uri: bolt://localhost:7687
username: neo4j
password: secret
register-suspend: true
register-virtual-thread: true

Ktor 3 users install GraphPlugin in the application module. The plugin stores the resolved state in application attributes and closes plugin-owned resources on application stop.

fun Application.module() {
install(GraphPlugin) {
tinkerGraph()
}
routing {
get("/users/{id}/cluster") {
val graph = graphPluginState().graphOperations
val service = AbuserDetectionService(graph)
val cluster = service.findAbuseCluster(GraphElementId.of(call.parameters["id"]!!))
call.respond(cluster)
}
}
}

If DI or bootstrap code already created the operations pair, pass it directly. Keep the default caller-owned lifecycle, or set closeOnStop = true when the plugin should close the objects. Resource ownership should be explicit; shutdown is a poor time to discover that nobody knows who owns the driver.

install(GraphPlugin) {
operations(syncOps, suspendOps, closeOnStop = true)
}

Spring Boot integration is property-plus-bean injection. Ktor integration is plugin-plus-application lifecycle. Pick the one that matches the service runtime.

If you are starting from a relational-only service, do not move everything into a graph at once. Find the feature where variable-depth traversal, shortest path, shared identity, or recommendation candidate generation is making relational queries hard to read. Move that feature first, keep the relational database as source of truth when it already owns the business data, start tests with TinkerGraph, and measure the production candidates with your workload.

Comments

Leave a note or reaction with your GitHub account.