Neo4j (Graph Database) β Part 1: Foundations & Core Concepts
maang.io System Design Series
What is this?
Picture a small town where every building is a house, and neighbours who know each other are joined by a private bridge built directly between their two front doors. Some bridges are one-way ("Alice lends money to Bob"), some are two-way ("Alice and Bob are friends"), and every bridge has a little label painted on it saying what kind of connection it is. Inside each house is a notepad of facts about who lives there; pinned to each bridge is a note about the relationship.
Now here's the crucial part. When you're standing in Alice's house and you want to visit her friends, you don't phone the town hall and ask it to search a giant registry of "who is connected to whom." You just look at the rack of keys by Alice's front door β one key per bridge leading out β and walk straight across. Visiting a friend-of-a-friend is just two walks. Visiting friends-five-hops-away is five walks. The town hall's central registry never enters into it.
That town is a graph database, and Neo4j is the most widely deployed one. Houses are nodes, bridges are relationships, the notepads are properties, the paint colour is a label. The rack-of-keys-by-the-door is the single idea that makes graph databases special β it's called index-free adjacency, and we'll come back to that town for all three chapters.
The one-line idea: A graph database stores relationships as direct physical pointers between records, so hopping from one entity to a connected one costs O(1) β a pointer you follow, not an index you search. Deep, connected questions ("friends-of-friends-of-friends," "is there a fraud ring linking these five accounts?") that would be a pile of expensive JOINs in SQL become a cheap walk across the graph.
1. Why graph databases exist β the pain of the deep JOIN
You already know how to model relationships in a relational database (the SQL & Data Modeling topic in this tier goes deep on it): you make a join table. A social network becomes a person table and a friendship(person_a, person_b) table. To find Alice's friends, you JOIN person to friendship. Fine.
Now the interviewer says: "Find everyone within four hops of Alice." In SQL that's a four-way self-join of the friendship table:
-- "Friends of friends of friends of friends" β the query graph DBs were born to kill.
SELECT DISTINCT f4.person_b
FROM friendship f1
JOIN friendship f2 ON f2.person_a = f1.person_b
JOIN friendship f3 ON f3.person_a = f2.person_b
JOIN friendship f4 ON f4.person_a = f3.person_b
WHERE f1.person_a = :alice;
Every join level asks the database to look up matches in an index β a B-tree descent that costs O(log N) in the whole friendship table, and worse, the intermediate result set explodes multiplicatively at each level. With 1,000,000 users averaging 50 friends each (50M friendships), a depth-4 query can generate billions of intermediate rows before the DISTINCT collapses them. Teams routinely watch these queries run for minutes, or simply time out. The relational model is superb at "give me all rows where a column equals X"; it is structurally bad at "follow the connections several steps out," because every step is a fresh index search over the entire dataset.
The graph model refuses to pay that tax. Because Alice's node holds direct pointers to her relationship records, and each of those points straight at the neighbour node, a four-hop expansion touches only the part of the graph you actually walk β proportional to the answer, not to the size of the database. That's the whole pitch, and it's why graph databases won a permanent seat next to relational and document stores.
π‘ Rule of thumb: if your killer queries involve variable-depth traversal, pathfinding, or pattern matching over connections β and especially if you find yourself writing three-, four-, five-way self-joins β a graph is the natural shape. If your queries are "filter and aggregate rows by column values," stay relational.
2. The mental model β nodes, relationships, properties, labels
Neo4j implements a native property graph. Four vocabulary words carry the entire model, so let's pin them down against the town.
- Node β an entity. A house.
(:Person),(:Company),(:Account). A node can have zero or more properties. - Relationship β a first-class, directed, typed connection between exactly two nodes. A bridge. It always has a start node, an end node, and a type in capitals by convention (
:KNOWS,:PURCHASED,:TRANSFERRED_TO). Crucially, in a property graph a relationship is a real stored thing you can put data on β not a foreign key you infer. - Property β a keyβvalue pair (
name: "Alice",since: 2019,amount: 250.00) attached to either a node or a relationship. The notepad and the bridge-note. - Label β a tag that groups nodes into a set and lets you index them (
:Person,:Fraudster). The paint colour. A node can wear several labels.
That picture is the database. There is no shredding entities across normalized tables and reassembling them with joins β the structure on disk mirrors the structure on the whiteboard. This is why people say graphs are "whiteboard-friendly": the diagram your architect drew is, almost literally, the schema.
Two contrasts worth carrying forward:
- vs. relational (SQL & Data Modeling): relationships are foreign keys you reconstruct with joins at query time. In a graph they're stored, named, and directly navigable.
- vs. document / key-value (see SQL vs NoSQL): a document store nests related data inside one document, which is great until relationships are many-to-many or you need to traverse them from either end. Graphs make the relationship the star, navigable in both directions.
3. How you actually use it β a first taste of Cypher
You query Neo4j with Cypher, a declarative, pattern-matching language (now standardized as openCypher / the ISO GQL standard). Its party trick is ASCII-art patterns: you literally draw the shape you're looking for. Nodes are () and relationships are -[]->.
// Create some of the town.
CREATE (alice:Person {name: 'Alice'})
CREATE (bob:Person {name: 'Bob'})
CREATE (alice)-[:KNOWS {since: 2019}]->(bob);
// The friends-of-friends question that terrified us in SQL β one readable line.
MATCH (me:Person {name: 'Alice'})-[:KNOWS]->()-[:KNOWS]->(fof:Person)
WHERE fof <> me
RETURN DISTINCT fof.name;
Read the MATCH pattern like a sentence: start at the Person named Alice, walk a KNOWS bridge to someone, walk another KNOWS bridge to a Person fof, give me those. Want depth "1 to 4 hops"? You don't add four joins β you write a variable-length pattern:
// Anyone reachable from Alice in 1 to 4 KNOWS hops.
MATCH (me:Person {name: 'Alice'})-[:KNOWS*1..4]->(reachable:Person)
RETURN DISTINCT reachable.name;
That *1..4 is the payoff of the whole model. The query planner turns it into a bounded traversal that pointer-chases through the graph. Notice the {name: 'Alice'} at the start: that single lookup uses an index to find where to begin. Everything after it is index-free walking β a distinction that becomes the heart of Part 2.
4. When to reach for it β and when not
A graph database is a specialist tool. It shines on a recognizable shape of problem and is the wrong answer for several others.
Reach for a graph when the connections are the point:
| Use case | Why a graph wins |
|---|---|
| Recommendations ("people who bought X also boughtβ¦") | Collaborative filtering is a 2β3 hop traversal over purchase edges. |
| Fraud / AML ring detection | Finding cycles and shared attributes across accounts is pathfinding β a nightmare in SQL. |
| Social networks & feeds | Friends-of-friends, mutual connections, shortest path between people. |
| Knowledge graphs & master data | Richly interlinked entities queried by arbitrary paths (this powers search panels and LLM retrieval). |
| Network / IT topology & permissions | "What breaks if this router dies?" / "Can user U reach resource R through any role?" are reachability queries. |
Do not reach for a graph when:
- Your workload is bulk aggregation / analytics over most rows ("sum revenue by region by month") β that's a columnar/OLAP job (e.g. Azure Synapse Analytics), not a traversal.
- You need massive write throughput or time-series ingestion at scale β reach for Apache Cassandra or Amazon DynamoDB, which partition and absorb writes horizontally (Neo4j scales writes through a single leader β Part 2).
- Your data is mostly independent records with few relationships β a relational or document store is simpler and cheaper; don't pay for a graph engine you won't traverse.
- Your primary access is full-text search β that's Elasticsearch / Apache Lucene territory (though Neo4j embeds Lucene for its own full-text indexes, as we'll see).
β οΈ The classic mistake is treating Neo4j as a general-purpose database and stuffing high-volume, weakly-connected data into it, then wondering why it doesn't scale writes like Cassandra. Graph databases trade horizontal write-scalability for traversal speed. Use them because your data is densely connected, not in spite of it.
5. Key takeaways
- A graph database stores relationships as first-class, directed, typed records with direct pointers between them β the town of houses joined by bridges. The structure on disk mirrors the whiteboard.
- Its superpower is deep, connected queries: friends-of-friends, pathfinding, ring detection, variable-depth traversal β the very queries that become exploding multi-way self-joins in SQL.
- The model is four words: nodes (entities), relationships (typed, directed connections that carry data), properties (keyβvalue facts on either), labels (groupings you can index).
- Cypher lets you draw the pattern you want (
(a)-[:KNOWS*1..4]->(b)); an index finds the starting node, and the rest is index-free walking. - Reach for a graph when connections are the query (recommendations, fraud, social, knowledge graphs); reach elsewhere for bulk analytics (Synapse), high-volume writes (Cassandra/DynamoDB), or search-first workloads (Elasticsearch).
Next, Part 2 opens the hood: exactly how Neo4j lays fixed-size records on disk so that "follow a bridge" is pure pointer arithmetic, how the read and write paths work, how ACID and the transaction log keep it honest, and how Causal Clustering replicates the graph.