Graph Modeling: Nodes, Relationships, Paths, and Traversal
Graph databases model connected data explicitly:
node -[relationship]-> node
They are useful when the relationship itself is part of the query, especially when queries traverse several hops. Typical workloads include social connections, fraud networks, recommendation graphs, dependencies, routing, organizational relationships, and knowledge graphs.
Start from the traversal
Suppose a bookstore asks:
Recommend books liked by people followed by Alice.
(Alice)-[:FOLLOWS]->(Bob)-[:LIKES]->(Book A)
|
`--------------->(Carol)-[:LIKES]->(Book B)
The requested operation is a path traversal. That is the reason to examine a graph model.
Nodes
Nodes represent entities such as:
Customer
Book
Author
Publisher
Account
Device
IP
A node normally has labels/types, properties, and a stable identity.
Conceptually:
(:Customer {
customer_id: 42,
name: "Alice"
})
Relationships
Relationships connect nodes:
(:Customer)-[:FOLLOWS]->(:Customer)
Relationships can carry properties:
(:Customer)-[:PURCHASED {
quantity: 2,
purchased_at: ...
}]->(:Book)
Use relationship properties only for facts that truly belong to the connection.
When an intermediate node is better
Suppose an order has its own identifier, payment/shipping state, several line items, refunds, and lifecycle transitions.
A direct edge loses too much structure:
(Customer)-[:PURCHASED]->(Book)
A better model is:
(Customer)-[:PLACED]->(Order)
(Order)-[:CONTAINS {quantity: 2}]->(Book)
The order is a first-class entity. Graph modeling does not mean turning every relational join table into an edge.
Labels, relationship types, and direction
Prefer meaningful types:
(:Customer)
(:Book)
(:Author)
-[:FOLLOWS]->
-[:LIKES]->
-[:WROTE]->
Avoid generic RELATED_TO relationships when the business meaning matters.
Relationships are directed:
(Alice)-[:FOLLOWS]->(Bob)
is different from:
(Bob)-[:FOLLOWS]->(Alice)
For symmetric concepts such as friendship, choose a consistent representation: one relationship queried as undirected, two directed relationships, or a first-class relationship entity.
Property or node?
A genre can be a property:
(:Book {genre: "Database"})
if it is only a label value.
It can be a node:
(:Book)-[:IN_GENRE]->(:Genre)
if genre has identity, metadata, hierarchy, or traversal behavior.
Choose from the queries, not from a universal modeling rule.
Graph versus relational joins
A relational model can represent the same facts with tables such as:
customers
follows
books
likes
SQL can traverse them with joins.
A graph database becomes attractive when path length varies, many relationship types matter, traversal depth is central, and pattern matching dominates the workload.
Do not choose a graph merely because the domain has relationships. Every relational system models relationships too.
Cypher pattern matching
Neo4j uses Cypher.
The example graph contains these nodes and relationships:
Customers
| customer_id | name |
|---|---|
| 42 | Alice |
| 73 | Bob |
| 90 | Carol |
Books
| book_id | title |
|---|---|
| 101 | Database Systems |
| 205 | SQL Practice |
Relationships
| start | relationship | end |
|---|---|---|
| Alice (42) | FOLLOWS | Bob (73) |
| Bob (73) | LIKES | Database Systems (101) |
| Bob (73) | LIKES | SQL Practice (205) |
MATCH (alice:Customer {customer_id: 42})
-[:FOLLOWS]->(:Customer)
-[:LIKES]->(book:Book)
RETURN DISTINCT book.title
ORDER BY book.title;
Result:
| book.title |
|---|
| Database Systems |
| SQL Practice |
The query visually resembles the graph pattern.
Runnable Neo4j demo
The repository already contains:
This NoSQL chapter adds:
scripts/neo4j/nosql_traversal_demo.py
Start Neo4j and run it:
cd scripts
bash setup/start_neo4j.sh
cd ..
python scripts/neo4j/nosql_traversal_demo.py
The demo creates customers/books, adds FOLLOWS and LIKES relationships, and executes a two-hop recommendation query.
Identity and uniqueness
Graph databases still need identity rules.
Examples:
Customer.customer_id unique
Book.book_id unique
Without constraints, repeated imports can create duplicate logical nodes. Define uniqueness constraints where the engine supports them.
MERGE semantics
In Neo4j, MERGE finds or creates a pattern. A full-pattern merge can have surprising behavior when only part of the pattern exists. It is often safer to establish unique node identities separately and then merge the relationship.
The existing repository demo shows why this matters.
Path length and branching factor
Fixed traversal:
Alice -> friend -> book
Variable traversal:
Alice -> friend -> friend -> friend -> ...
Variable-depth traversal is powerful and can be expensive.
Prefer an explicit maximum depth:
MATCH p =
(:Customer {customer_id: 42})
-[:FOLLOWS*1..3]->
(:Customer)
RETURN p;
For the sample graph, the result is:
| start node | relationship | end node |
|---|---|---|
| Alice (42) | FOLLOWS | Bob (73) |
If each node has 100 outgoing edges:
depth 1 -> 100 candidates
depth 2 -> 10,000
depth 3 -> 1,000,000
Graph overlap may reduce this, but path depth and node degree are core performance dimensions.
Supernodes
A supernode has extremely high degree.
Example:
(:Country {name: "US"})
connected to hundreds of millions of users.
Traversing through it may be expensive. Alternatives can include property filtering, hierarchy, relationship partitioning, or avoiding traversal through global hubs.
Recommendation graph
Customer
| LIKES
v
Book
^
| LIKES
Other Customer
A collaborative query can ask:
Find books liked by customers who like some of the same books as Alice.
Production recommendation systems may still combine graph features with ranking models, embeddings, popularity, and business rules.
Fraud graph
Possible nodes:
Account
Card
Device
IP
Email
Phone
Relationships:
USED_DEVICE
USED_CARD
LOGGED_IN_FROM
HAS_EMAIL
HAS_PHONE
A fraud query might ask:
Find accounts connected within two hops through the same device or payment card.
Graph traversal can reveal shared infrastructure that is awkward to discover through repeated joins.
Dependency graph
Nodes:
Service
Database
Queue
Library
Relationships:
CALLS
DEPENDS_ON
READS_FROM
WRITES_TO
Questions include:
- what breaks if database X is unavailable?
- which services depend indirectly on library Y?
Graphs fit impact analysis well.
Hierarchies and cycles
Graphs can support ancestors, descendants, and multiple parents. They can also contain cycles:
A -> B -> C -> A
Some domains allow cycles. Others do not.
An organizational hierarchy may require acyclicity. The database may not automatically enforce every global graph invariant, so application/procedure logic can still be necessary.
Relationship uniqueness
If Alice can follow Bob only once, prevent duplicate logical relationships through supported constraints/patterns and transaction logic.
Duplicate edges can distort counts, recommendations, and traversals.
Traversal versus aggregation
Graph databases excel at connectivity.
They may not be the best system for:
SUM(revenue) by country for 10 years
Use analytical databases or warehouses for broad scans and aggregations. A production system can use both.
Graph as a derived projection
A common architecture:
transactional database
|
v
CDC/batch
|
v
graph projection
|
v
fraud/recommendation queries
The graph becomes a traversal-optimized projection while the transactional database remains the source of truth.
If updates are asynchronous, define tolerated lag, idempotent updates, ordering expectations, and a rebuild procedure.
Indexes and starting nodes
Graph queries often begin from a known node:
Customer.customer_id = 42
An index/constraint helps find the start node. Traversal then follows relationships.
A poor starting lookup can make every graph query slower.
EXPLAIN and PROFILE
Neo4j provides planner/profiling tools:
EXPLAIN ...
and:
PROFILE ...
Inspect starting-node lookup, expansions, rows produced, and expensive Cartesian products.
Avoid Cartesian products
A pattern such as:
MATCH (a:Customer), (b:Book)
RETURN a, b
ORDER BY a.name, b.title;
The sample graph has three customers and two books, so the query returns six customer/book pairs. For example:
| customer | book |
|---|---|
| Alice | Database Systems |
| Alice | SQL Practice |
| Bob | Database Systems |
| Bob | SQL Practice |
| Carol | Database Systems |
| Carol | SQL Practice |
without relationship/filter constraints can create a large Cartesian product.
Graph query languages can express very expensive patterns; model and profile them deliberately.
OPTIONAL MATCH
OPTIONAL MATCH is useful when a relationship may be absent:
MATCH (c:Customer)
OPTIONAL MATCH (c)-[:LIKES]->(b:Book)
RETURN c.name, b.title
ORDER BY c.name, b.title;
Input relationships: Bob likes both books; Alice and Carol have no LIKES relationships.
Result:
| c.name | b.title |
|---|---|
| Alice | NULL |
| Bob | Database Systems |
| Bob | SQL Practice |
| Carol | NULL |
Customers without matching books remain in the result.
Aggregation
Graph queries can aggregate:
MATCH (:Customer)-[:LIKES]->(b:Book)
RETURN b.title, count(*) AS likes
ORDER BY likes DESC, b.title;
Result:
| b.title | likes |
|---|---|
| Database Systems | 1 |
| SQL Practice | 1 |
Use graphs where traversal is the primary value, not just because aggregation exists.
Shortest paths and graph algorithms
Graph systems can support shortest paths, PageRank, community detection, centrality, and similarity.
These may have very different resource profiles from online point traversals. Do not run large graph algorithms on a production transactional graph without capacity planning.
Import workflow
A safe bulk-load sequence:
- load nodes with stable IDs,
- create/verify uniqueness constraints,
- load relationships referencing stable IDs,
- validate node and relationship counts,
- inspect high-degree nodes.
Identity mistakes during import are difficult to repair later.
Delete semantics
Deleting a node with relationships may require explicit handling.
Neo4j distinguishes:
DELETE n
from:
DETACH DELETE n
The repository's delete demo shows the difference.
Multi-tenancy and privacy
A shared graph needs tenant boundaries.
One model:
(:Customer {
tenant_id: "acme",
customer_id: 42
})
Every query must maintain tenant scope.
Relationships can themselves be sensitive:
person -> clinic
person -> support_group
Protect topology as well as node properties.
Modeling workflow
- State the traversal question in plain language.
- Identify node types.
- Identify relationship types and direction.
- Decide which facts belong on relationships.
- Introduce intermediate nodes when entities have their own lifecycle.
- Define stable identifiers and uniqueness.
- Estimate degrees and path depth.
- Identify potential supernodes.
- Define tenant/security boundaries.
- Profile representative queries.
- Decide source-of-truth versus derived graph.
- Define synchronization and rebuild behavior.
Common mistakes
"Our data has relationships, so use a graph"
Every data model has relationships.
Generic RELATED_TO relationships
Business meaning disappears.
Unbounded traversal
Query expansion can explode.
Duplicate nodes after import
Identity was not constrained.
Order represented only as a PURCHASED edge
Independent order state/lifecycle is lost.
Graph used for global warehouse analytics
Wrong workload.
Derived graph has no rebuild process
Synchronization failures become permanent.