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.