Storage Schema and the Property Graph
In one line: the interesting part is not the tables. It is that the chapter says "use a graph database" and then shows you how to build one out of two relational tables, which is a genuinely useful thing to know.
The relations
| Relation | Holds |
|---|---|
| User | Data about a user. A user can also be a follower or friend of other users |
| Entity | Data about entities, such as pages and groups |
| Feed_item | Posts created by users |
| Media | Information about the media content |
Entity as a separate relation is right, and it makes the follow relation heterogeneous
Splitting User from Entity (pages and groups) is correct — they have genuinely different attributes. A page has a Description and a Creator; a user has an Email and a LastLogin.
But it creates a problem the relational schema cannot express cleanly: you follow both. A single "follows" table would need a foreign key pointing at either User or Entity, which relational schemas handle badly — you end up with nullable columns, a discriminator, or two separate tables.
That awkwardness is precisely why the design reaches for a graph representation for relationships. In a graph, a node is a node; the edge does not care whether its endpoints are users or pages.
Worth noting because it is a common real design pressure: relational schemas model entities well and heterogeneous relationships badly, and the follow relation in any social product is heterogeneous the moment you add pages, groups, topics, or hashtags.
Media as a separate relation confirms the reference model
Media is its own relation, keyed by Media_ID and referencing Feed_Item_ID — so a post points at its media rather than containing it.
That is Lesson 3's correction expressed in the schema. The media record is metadata; the bytes live in blob storage. A post referenced by ten million feeds still has one row here and one object there.
If you needed a single line of evidence that the 56 PB figure double-counts, this is it: the schema stores media once.
The property graph
User and post data are structured and stored in a relational database. A graph database is used to model relationships such as friendships and follower connections. For this purpose, we follow the property graph model.
We can think of a graph database consisting of two relational tables: one for vertices that represent users, and one for edges that denote relationships among them. The schema uses the PostgreSQL JSON data type to store the properties of each vertex or edge.
The design's worked example:
Graph: 782 --Following--> 399
Relationship table:
Relation_ID | Relation_from | Relation_towards | Label | Properties
111 | 782 | 399 | Following | { ...JSON... }
Two tables and a JSON column is a graph database — and knowing that is genuinely useful
The property graph model has exactly two element types: vertices with properties, and labelled directed edges with properties. This schema expresses both.
| Graph concept | Relational representation |
|---|---|
| Vertex | A row in User with a JSON properties column |
| Edge | A row in Relationship with from, towards, Label, properties |
| Edge type | The Label column — Following, Friend, Member_of |
| Heterogeneous nodes | Absorbed into the JSON properties |
Three things this buys, and they answer the previous callout's problem:
Heterogeneity. The Label column means one table holds friendships, follows, group memberships, and blocks. No schema change to add a relationship type.
Schema flexibility. JSON properties mean per-edge attributes — when the follow started, whether notifications are muted — without altering the table.
Operational simplicity. It is PostgreSQL. You already know how to back it up, replicate it, and monitor it. Adopting a dedicated graph database means a new operational surface.
The honest framing: you do not need a graph database to store a graph. You need one when your queries are graph queries — multi-hop traversals, shortest paths, triangle counting — because expressing those in SQL means recursive CTEs or repeated self-joins, and performance degrades badly with depth.
So the right question is not "is this a graph?" but "how many hops do my queries traverse?"
For a newsfeed, the answer is one hop — which is why this works
Look at what Lesson 8's generation service actually asks:
"Who does Alice follow?" -> SELECT Relation_towards
FROM Relationship
WHERE Relation_from = Alice AND Label = 'Following'
One hop. A single indexed lookup returning a list. No traversal, no recursion, no path-finding.
That is the whole graph workload for feed generation, and a relational table with an index on (Relation_from, Label) serves it perfectly.
Where you would genuinely want a graph engine:
- Friend-of-friend suggestions — two hops, and the intermediate result explodes.
- Shortest connection path — LinkedIn's "3rd degree," which is unbounded traversal.
- Community detection — global graph algorithms.
Those are recommendation and discovery features, not feed generation. Which suggests the right architecture: relational for the one-hop feed path, a graph engine alongside for multi-hop discovery.
Compare that building block, which used FlockDB — a dedicated graph store — for the same one-hop query. The justification there was different: not traversal depth but adjacency list size, since enumerating 50 million followers for fan-out is a scale problem rather than a graph problem.
Two different reasons to reach for a graph store, and only one of them is about graphs.
The direction of the edge determines which query is cheap
Relation_from and Relation_towards make edges directed, which is correct — following is asymmetric.
But it means the two queries you need have different costs unless you index for both:
"Who does Alice follow?" -> index on Relation_from -> feed GENERATION "Who follows Alice?" -> index on Relation_towards -> FAN-OUT on write
Lesson 4 needs both, on opposite paths. Pull needs who does Alice follow; push needs who follows Bob.
And their cardinalities are wildly different. Alice follows 550 things; a large page is followed by tens of millions. So the second index has entries whose result sets span five orders of magnitude — which is exactly the skew that broke push fan-out.
A directed edge needs two indexes, and in a social graph they have completely different size distributions. Worth saying, because a design that indexes only one direction has silently chosen a fan-out strategy.
Key takeaway
Separating User from Entity is right and makes the follow relation heterogeneous, which relational schemas model badly — hence the graph representation. The property graph is two tables plus JSON columns: vertices, and labelled directed edges whose Label absorbs friendships, follows, and memberships without schema changes. You do not need a graph database to store a graph — you need one when your queries traverse multiple hops, and feed generation is one hop, so an indexed relational table serves it perfectly. Multi-hop discovery features are the real case for a graph engine. And a directed edge needs two indexes, one per direction, serving pull and push respectively — with result-set sizes spanning five orders of magnitude, which is the same skew that broke fan-out.
Next: the generation service, step by step.