Data Engineering  /  Data Modeling

🧱 Data Modeling 20 guides · updated 2026

From ER diagrams to data mesh — relational, dimensional, NoSQL, and governance-driven modeling for building data platforms people can trust.

Graph Data Modeling: When Relationships Are the Data

Some questions are easy to ask and brutally hard to answer in SQL. “Which of my customers know each other?” “Is this new account connected — through any chain of shared devices, addresses, or payment methods — to a known fraudster?” “What’s the shortest chain of introductions from me to this executive?” Each of these is a question about paths through relationships, and relational databases, despite the name, make paths expensive: every hop is another self-join, and query cost explodes with depth.

Graph databases invert the trade. Relationships are stored as first-class, directly-traversable objects, so following a connection costs the same whether it’s hop one or hop seven. This post covers how to model for them: the property graph model, the design process, patterns and pitfalls, and an honest account of when not to use a graph.

The Property Graph Model in Two Minutes

The dominant model (Neo4j, Amazon Neptune, Memgraph) has four elements:

That last point is the model’s quiet superpower. In relational design, a relationship with attributes forces a junction table; in a graph, the relationship simply carries its data:

(alice:Person {name:"Alice"})-[:TRANSFERRED {amount: 5000, at: "2026-06-12"}]->(bob:Person {name:"Bob"})

When a Graph Beats a Relational Model

The honest test is not “is my data connected?” — all data is connected — but “do my queries traverse a variable or deep number of hops?” Compare the question “who are Alice’s friends-of-friends-of-friends?” in both worlds.

SQL, three self-joins deep and growing per level:

SELECT DISTINCT f3.friend_id
FROM friendships f1
JOIN friendships f2 ON f2.person_id = f1.friend_id
JOIN friendships f3 ON f3.person_id = f2.friend_id
WHERE f1.person_id = 42;

Cypher (the graph query language), where depth is a parameter, not a rewrite:

MATCH (a:Person {id: 42})-[:FRIENDS_WITH*1..3]-(fof)
RETURN DISTINCT fof;

The graph engine follows stored pointers from node to node — no index lookups per hop, no join planning. Workloads where this dominates: fraud rings, social networks, recommendation engines (“customers who bought what you bought also bought…”), knowledge graphs, network/IT topology, supply-chain tracing, and access-control resolution (“does this user, through any role chain, have permission X?”).

If your queries are aggregations over millions of uniform rows (“total sales by region”), a graph is the wrong tool — columnar warehouses crush graphs at scan-and-aggregate work.

The Modeling Process: Nouns, Verbs, Questions

Graph modeling is closer to natural language than any other paradigm, which makes whiteboard sessions with domain experts remarkably productive:

  1. Write the questions first — as sentences. “Find accounts sharing a device with a flagged account.”
  2. Nouns become nodes: Account, Device, Person.
  3. Verbs become relationships: USES, OWNS, FLAGGED_AS.
  4. Check each question is a path through your sketch. If answering requires assembling data that isn’t connected, add the missing relationship.

The most important design instinct: things you’ll traverse through must be nodes, not properties. Suppose you model a person’s email as a property: (:Person {email: "x@y.com"}). Now “find people sharing an email” is a full scan comparing property values — the graph gives you nothing. Model the email as a node — (:Person)-[:HAS_EMAIL]->(:Email {addr}) — and shared emails become shared nodes: two OWNS arrows converging on one point, traversable in constant time. The rule generalizes: attributes are for filtering, nodes are for connecting. Whenever a value can link entities (emails, devices, addresses, skills, tags), promote it to a node.

A Worked Example: Fraud Ring Detection

Fraud teams discovered graphs early because fraud is structurally invisible in rows. Individually, each account looks fine; the fraud is the shape — many accounts quietly sharing infrastructure.

Account A1

Device 77

Account A2

Card •••4921

Account A3

Address: 14 Rose St

Account A4 ⚑ flagged

Model: (:Account)-[:USES]->(:Device), (:Account)-[:PAYS_WITH]->(:Card), (:Account)-[:REGISTERED_AT]->(:Address). The killer query — “find accounts within three hops of a flagged account through any shared identifier” — is a few lines of Cypher and runs in milliseconds, because it just walks pointers outward from the flagged node. The relational equivalent is a UNION of self-joins per identifier type per depth, and most teams simply never build it.

Patterns and Pitfalls

Qualify your relationships. RATED {stars: 4} beats separate LIKED/DISLIKED types when the value matters; but prefer specific relationship types (ACTED_IN, DIRECTED) over one generic RELATED_TO with a type property — traversal by relationship type is the engine’s fastest filter.

Time on relationships. Facts change: employment, ownership, friendship. Put from/to properties on relationships to keep history traversable: -[:WORKED_AT {from: 2019, to: 2023}]->. “Who worked together at Acme in 2021?” stays a path query.

Beware supernodes. A node with millions of relationships — a (:Country {name:"India"}) node every user connects to, or a celebrity in a social graph — turns every traversal that touches it into a scan of its relationship list. Mitigations: don’t model low-selectivity categories as nodes (make country a property or an index instead — connecting through it tells you almost nothing anyway), and for legitimate hubs, filter by relationship type/direction early or shard the hub.

Don’t graph your whole enterprise. The successful pattern is a graph for the relationship-shaped subdomain (fraud, recommendations, the knowledge graph), fed from systems of record that remain relational or document-based. Graphs earn their keep on traversal queries; they’re a poor system of record for bulk transactional data.

Getting Started Checklist

  1. Write your ten questions as sentences; confirm at least half involve variable-depth or multi-type traversal. If not, stop — use SQL.
  2. Sketch nodes (nouns) and relationships (verbs); walk every question as a path on the sketch.
  3. Promote any “connecting” attribute (email, device, tag) to a node.
  4. Add time properties to relationships that change.
  5. Load a realistic sample and profile your worst query for supernodes before committing.

Graph modeling rewards a specific kind of thinking: instead of asking “what are my entities?”, you ask “what do I want to walk between?” When the questions are about connection — who touches whom, what leads to what, how close is A to B — no other model comes close. When they aren’t, no amount of graph enthusiasm will beat a well-indexed join. Knowing which situation you’re in is the modeling skill this post was really about.