1. The Headline
Millions of infinitely nestable blocks that work perfectly offline.
Notion replaced traditional documents (like Word or Google Docs) with a revolutionary block-based canvas. Everything is a block: a paragraph, a checkbox, a row in a database, or an entire page. This extreme flexibility required a totally non-standard data model and a frontend architecture designed explicitly for offline usage.
2. Requirements and Constraints
Functional Requirements:
- Everything on the canvas can be dragged, dropped, nested, and converted into different types.
- Changes sync across devices instantly.
- The app must work flawlessly on airplanes or subways (Offline Mode).
Non-Functional Requirements:
- Perceived Speed: Keystrokes and drag-and-drops must feel instant, regardless of internet quality.
- Data Integrity: Resolving offline edits cannot destroy the complex parent-child tree structure of blocks.
The Ultimate Constraint: Unlike Google Docs, which deals with a linear string of text, Notion deals with a massive graph of tree nodes. Loading a single Notion page might require fetching thousands of deeply nested blocks from a database. Querying relational databases recursively to reconstruct a tree is exceptionally slow.
3. The Naive Design & Where It Breaks
A naive approach to a block-based document:
- Store blocks in a PostgreSQL database with a
parent_idcolumn:blocks (id, content, type, parent_id). - When a user opens a page, the server runs a recursive SQL query (
WITH RECURSIVE) to fetch the page block and all its children. - The server sends a massive JSON object to the frontend.
Where this breaks:
- Database Spikes: Recursive queries are slow. If a page has 5,000 blocks nested 10 levels deep, fetching the page will take seconds.
- Offline Failure: If the user is offline, the frontend cannot query PostgreSQL. The app becomes a read-only shell or crashes.
- Save Contention: If Alice drags a block from index 2 to index 10, the server must rewrite the
ordervalues for 8 different rows in the database, locking them and causing race conditions with Bob.
4. The Real Architecture: Layer by Layer
Normal operation: The client streams video from the CDN and maintains a persistent WebSocket/MQTT connection for real-time scores.
The Data Model (PostgreSQL + Redis)
Notion models the entire universe as a flat map of blocks. A block looks like this:
{
"id": "block-123",
"type": "text",
"properties": { "title": [["Hello World"]] },
"content": ["block-456", "block-789"],
"parent_id": "page-001"
}Crucially, the parent block maintains a content array of its children's IDs. The children also know their parent_id.
Lazy Loading and Pointers
When you load a page, the backend (PostgreSQL heavily cached by Redis) does not fetch the entire tree recursively. It fetches the Page block. The frontend sees the content array (which contains IDs). The frontend then issues a second request to fetch just those specific child IDs.
If a block is folded (like a toggle list), the frontend does not fetch its children until the user clicks the toggle. This lazy-loading keeps initial page loads blazing fast.
5. The Hard Problem
The Offline-First Sync Engine.
If you are offline on a subway, you can create a new page, write 10 paragraphs (blocks), and drag them around. When you reconnect, the backend must resolve these tree mutations without corrupting the document structure.
6. What This Means for the Client (Frontend)
Notion is the textbook definition of a Local-First Architecture. The source of truth for the user is the local device, not the cloud server.
The Local Cache (IndexedDB / SQLite)
The Notion frontend runs a complete caching engine locally. When you fetch a block, it is saved in IndexedDB (Web) or SQLite (Desktop/Mobile). If you go offline, the frontend simply queries IndexedDB instead of the network. Because the data model is just a flat map of objects linked by IDs, the frontend can reconstruct the tree entirely from local data.
Optimistic Writes and the Transaction Queue
When you type or drag a block, the frontend does not ask the server for permission.
- It applies the mutation to the local IndexedDB instantly.
- It re-renders the React UI instantly.
- It pushes a "Transaction" object into a local Queue.
- Example Transaction:
set(block-123, properties.title, "Hello") - Example Transaction:
listAfter(page-001, content, block-123, block-456)
- Example Transaction:
If you are offline, this queue simply grows.
Reconnection and Conflict Resolution
When internet returns, the frontend flushes the queue to the backend API via a WebSocket.
The backend processes these atomic transactions. If there is a conflict (e.g., Alice deleted the page that Bob just added a block to), Notion uses Last-Write-Wins (LWW) based on timestamps for most property changes. For structural changes (the content array), Notion uses a specialized algorithm to merge the arrays safely, ensuring blocks are never orphaned.
7. Failure Modes & Graceful Degradation
- Database Degradation: Because every read goes through a massive Redis cache layer, PostgreSQL can experience heavy load or brief downtime without the user noticing, as long as they are reading cached blocks.
- WebSocket Drops: The local transaction queue ensures that transient network drops (like walking into an elevator) are completely invisible to the user. The queue quietly catches up in the background when the connection is restored.
8. Numbers & Tradeoffs
- Architecture: Local-First flat map of blocks with a transaction queue.
- Tradeoff: By making everything a block (even individual paragraphs), Notion generates billions of rows in their PostgreSQL database. This forced them to aggressively shard their databases early on. The tradeoff is extreme UI flexibility at the cost of massive database infrastructure complexity.
9. How to Use This in an Interview
If an interviewer asks you to design a complex collaborative canvas like Notion, Trello, or Jira:
"To handle deeply nested, flexible content, we should model the data as a flat map of Blocks linked by IDs, rather than storing monolithic HTML strings. To ensure fast load times, the frontend must lazy-load child blocks on demand."
"To achieve true Offline-First capabilities, the frontend must act as the primary database using IndexedDB/SQLite. All user actions are Optimistic Writes applied locally and placed in a Transaction Queue. When the network connects, the queue flushes to the backend, which applies Last-Write-Wins and array merging to resolve conflicts."
10. Sources
- The data model behind Notion
https://www.notion.so/blog/data-model-behind-notion - Sharding PostgreSQL at Notion
https://www.notion.so/blog/sharding-postgres-at-notion