This tutorial demonstrates how to build a simple social network application using the Pathway graph database. We will cover:
- Setting up the database
- Defining a schema-less data model
- Seeding data (Users, Posts, interactions)
- Running graph queries using the fluent traversal API
First, import the necessary packages:
package main
import (
"context"
"fmt"
"log"
"github.com/google/uuid"
"github.com/npclaudiu/pathway"
)
func main() {
// Indexes are optional. Configure only properties used by FindNodes.
db, err := pathway.OpenWithOptions(":memory:", pathway.Options{
// DurabilitySync is the default and is shown explicitly here.
Durability: pathway.DurabilitySync,
Indexes: []pathway.IndexDefinition{
{Label: "User", Property: "username"},
},
})
if err != nil {
log.Fatal(err)
}
defer db.Close()
ctx := context.Background()
// ...
}Index definitions are persisted. On a later open, pathway.Open preserves the
stored definitions. Passing a non-nil Options.Indexes slice instead makes it
the desired set: newly added indexes are rebuilt from existing nodes and
removed indexes are dropped atomically. FindNodes returns no matches for an
unindexed label/property pair.
DurabilitySync waits for each successful Update to be synchronized to
stable storage. For replayable bulk imports, DurabilityNoSync can reduce
commit latency, but a process or machine crash may lose recent updates that
already returned successfully. It does not change transaction atomicity, and
it does not relax schema or index-definition migrations.
Pathway is schema-less, but conceptually we will model:
- Nodes:
User,Post,Comment - Edges:
FOLLOWS(User -> User)POSTED(User -> Post)LIKED(User -> Post)COMMENTED(User -> Comment)ON(Comment -> Post)
We use db.BulkUpdate to seed the graph in one atomic commit. Its writer caches
edge endpoint validation, which is especially useful when many imported edges
share nodes. Endpoint validation checks node-key existence without copying or
decoding labels; each distinct endpoint is probed at most once per callback.
func seedData(ctx context.Context, db *pathway.Database) (map[string]uuid.UUID, map[string]uuid.UUID, error) {
users := make(map[string]uuid.UUID)
posts := make(map[string]uuid.UUID)
err := db.BulkUpdate(ctx, func(writer *pathway.BulkWriter) error {
// 1. Create Users
names := []string{"Alice", "Bob", "Charlie", "Dave", "Eve"}
for _, name := range names {
id := uuid.New()
users[name] = id
if err := writer.PutNode(id, "User"); err != nil {
return err
}
if err := writer.SetProperties(id, map[string]any{
"username": name,
"age": 25,
}); err != nil {
return err
}
}
// 2. Create Posts
post1 := uuid.New()
posts["AliceIntro"] = post1
if err := writer.PutNode(post1, "Post"); err != nil {
return err
}
if err := writer.SetProperties(post1, map[string]any{"content": "Hello World"}); err != nil {
return err
}
post2 := uuid.New()
posts["BobUpdate"] = post2
if err := writer.PutNode(post2, "Post"); err != nil {
return err
}
if err := writer.SetProperties(post2, map[string]any{"content": "Bob is here"}); err != nil {
return err
}
// 3. Create Edges
edges := []struct {
from, to uuid.UUID
label string
}{
{users["Alice"], users["Bob"], "FOLLOWS"},
{users["Alice"], users["Charlie"], "FOLLOWS"},
{users["Bob"], users["Charlie"], "FOLLOWS"},
{users["Alice"], posts["AliceIntro"], "POSTED"},
{users["Bob"], posts["AliceIntro"], "LIKED"},
}
for _, edge := range edges {
if _, err := writer.PutEdge(edge.from, edge.to, edge.label); err != nil {
return err
}
}
return nil
})
return users, posts, err
}BulkUpdate uses the configured durability mode and commits exactly once. Any
callback or writer-operation error aborts the complete batch. BulkWriter
remembers its first operation error, so accidentally ignoring one cannot commit
the operations staged before it.
Pathway provides a fluent traversal interface inspired by Gremlin. Normal node
results are pathway.Node values; ID, property, and path projections return
UUIDs, scalars, and typed paths, respectively.
func findFriends(db *pathway.Database, aliceID uuid.UUID) {
g := pathway.NewTraversalSource(db)
// Query: Start at Alice -> Outgoing "FOLLOWS" edges -> Neighbor Nodes
results, err := g.V(aliceID.String()).Out("FOLLOWS").ToList()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Alice follows %d people:\n", len(results))
for _, result := range results {
friend := result.(pathway.Node)
fmt.Printf("- %v (Label: %s)\n", friend.ID, friend.Label)
}
}Passing one or more edge labels to Out or In restricts Pebble to the exact
adjacency range for each unique label. Multiple labels are returned in
deterministic adjacency-key order, not caller argument order. Omitting labels
scans every incident edge for that direction.
Find people Alice follows, and then who they follow.
results, err := g.V(aliceID.String()).
Out("FOLLOWS"). // 1st Hop (Friends)
Out("FOLLOWS"). // 2nd Hop (Friends of Friends)
ToList()
if err != nil {
log.Fatal(err)
}Traverse backwards using In().
// Start at Post -> Incoming "LIKED" edge -> User
results, err := g.V(postID.String()).
In("LIKED").
ToList()
if err != nil {
log.Fatal(err)
}Complex traversal combining multiple edge types.
// Alice -> (Follows) -> Friends -> (Posted) -> Posts
results, err := g.V(aliceID.String()).
Out("FOLLOWS").
Out("POSTED").
ToList()
if err != nil {
log.Fatal(err)
}Use IDs when the caller needs only UUIDs. Neighbor labels are loaded lazily,
so this avoids one node-label point read per traversed edge and is especially
useful for high-degree nodes and multi-hop traversals.
friendIDs, err := g.V(aliceID.String()).
Out("FOLLOWS").
IDs().
ToList()
if err != nil {
log.Fatal(err)
}Each result is a uuid.UUID. Use ordinary ToList when labels are needed;
HasLabel and Path also materialize labels by design.
Values emits one typed scalar for every requested property that exists. The
requested key order is preserved and missing properties are skipped.
names, err := g.V(aliceID.String()).
Out("FOLLOWS").
Values("username").
ToList()
if err != nil {
log.Fatal(err)
}Path returns a pathway.Path. Each entry has a Kind, ID, and Label;
edge entries also set Other to the endpoint reached by that step.
paths, err := g.V(aliceID.String()).Out("FOLLOWS").Path().ToList()
if err != nil {
log.Fatal(err)
}
for _, result := range paths {
path := result.(pathway.Path)
for _, element := range path {
fmt.Printf("%s %s %s\n", element.Kind, element.Label, element.ID)
}
}Run the complete example or the integration test suite:
go run ./examples/social_network
go test ./tests -v