Skip to main content

Neo4j Architecture & Native Graph Storage

Module Duration: 6-8 hours Learning Style: Deep Technical + Implementation Details + Performance Analysis Outcome: Understand how Neo4j achieves O(1) traversals and ACID guarantees at the storage layer

Introduction: What Makes Neo4j “Native”?

Most databases were built for tables, then retrofitted for graphs. Neo4j was designed from day one for graphs. Key Difference: Index-free adjacency
Performance: 1000x faster for multi-hop traversals! This module reveals how Neo4j achieves this at the storage level.

Part 1: High-Level Architecture

System Layering

Core Components

1. Cypher Parser
  • Converts Cypher query string → Abstract Syntax Tree (AST)
  • Validates syntax
2. Query Planner
  • Generates execution plan (like SQL EXPLAIN)
  • Cost-based optimization (estimates row counts, cardinality)
  • Rule-based optimization (predicate push-down, etc.)
3. Execution Engine
  • Executes query plan using operators (Scan, Filter, Expand, etc.)
  • Pipelined execution (streaming results)
4. Transaction Manager
  • Ensures ACID properties
  • Write-Ahead Log (WAL) for durability
  • Locking for isolation
5. Storage Engine
  • Native graph storage (nodes, relationships, properties)
  • Page cache (in-memory buffer pool)
  • Indexes (for lookups)

Part 2: Storage Layout

Store Files

Neo4j stores graph data in multiple fixed-size record stores:
Key Principle: Fixed-size records enable O(1) access via record ID.

Node Store

Node Record Format (15 bytes in Neo4j 4.x):
Fields:
  • In Use (1 bit): Is this record active? (0 = deleted, 1 = active)
  • Next Rel (35 bits): Pointer to first relationship (ID)
  • Next Prop (36 bits): Pointer to first property (ID)
  • Labels (5 bytes): Inline label storage or pointer to label array
Example: Node with ID = 42:
Accessing Node 42:
Time Complexity: O(1)—direct calculation, no scanning!

Relationship Store

Relationship Record Format (34 bytes in Neo4j 4.x):
Fields:
  • First Node: Source node ID
  • Second Node: Target node ID
  • Rel Type: Relationship type ID (index into relationship type store)
  • First Prev/Next Rel: Doubly-linked list for source node’s relationships
  • Second Prev/Next Rel: Doubly-linked list for target node’s relationships
  • Next Prop: Pointer to first property
Why Doubly-Linked Lists? Each node maintains a linked list of its relationships:
Traversal: To find all relationships for Node A:
  1. Read Node A’s record → get Next Rel = 100
  2. Read Relationship 100 → check if First Node == A or Second Node == A
  3. Follow appropriate Next Rel pointer (100 → 101 → NULL)
Time Complexity: O(D), where D = degree of node (number of relationships) Critical Insight: Independent of total graph size!
  • In relational DB: O(log N) per relationship (index scan across all edges)
  • In Neo4j: O(D) (only scan node’s own relationships)
Example:
  • Graph with 1 billion relationships
  • Node A has 10 relationships
  • Relational: O(log 10⁹) ≈ 30 operations
  • Neo4j: O(10) = 10 operations
Speed-up: 3x, and scales better!

Property Store

Property Record Format (25 bytes):
Property Types: Property Chain: Properties form a singly-linked list:
Reading All Properties:

String Store

Stores strings > 12 characters: String Record (128 bytes, stores 120 chars + metadata):
Long Strings (> 120 chars): Stored across multiple records forming a linked list:

Dense Node Optimization

Problem: Nodes with many relationships (high degree) slow down traversals. Example: Celebrity node with 1M followers
Solution: Relationship Group Store (introduced Neo4j 2.1) When a node exceeds a threshold (default: 50 relationships), Neo4j creates relationship type groups:
Traversal:
Process:
  1. Read celeb node → Get Next Rel Group = 7000
  2. Scan relationship groups for FOLLOWED_BY (typically < 10 types)
  3. Follow First Outgoing pointer
  4. Scan only FOLLOWED_BY relationships (not all 1M!)
Performance: From O(1M) to O(types) + O(followers of type) = much faster!

Part 3: Indexes

Label Indexes (Default)

When you create a label, Neo4j automatically creates an index for lookups:
Without Index:
With Label Index:
Storage: Labels are stored in the Node Record (5 bytes for labels):
Label IDs map to names in neostore.labeltokenstore.db.

Property Indexes

Create Index:
Index Structure (Neo4j 4.x): Native Index (custom B+tree variant) Index File: schema/index/lucene-*/1/ (despite “lucene” in path, it’s native in 4.x+) B+Tree Structure:
Query:
Process:
  1. Look up “Bob” in index → O(log P)
  2. Get node ID (20)
  3. Read node 20 → O(1)
Total: O(log P) << O(P) (full scan)

Composite Indexes

Create:
Use Case:
Index Key: Composite key (name, age)("Alice", 30) Benefit: Single index lookup instead of two separate lookups + intersection.

Full-Text Indexes

Create:
Query:
Backend: Apache Lucene (inverted index) Use Cases:
  • Fuzzy search
  • Tokenization (split “software engineer” → [“software”, “engineer”])
  • Relevance scoring (TF-IDF)

Vector Indexes (Neo4j 5.x)

Create:
Query (Nearest neighbor search):
Backend: HNSW (Hierarchical Navigable Small World) graph for approximate nearest neighbor (ANN) search Use Cases:
  • Recommendation systems (user/item embeddings)
  • Semantic search (document embeddings)
  • Image similarity (CNN features)

Part 4: Transaction Management

ACID Guarantees

Neo4j provides full ACID transactions:
  • Atomicity: All-or-nothing (commit or rollback)
  • Consistency: Constraints enforced (uniqueness, existence)
  • Isolation: Transactions don’t interfere (locking)
  • Durability: Committed transactions survive crashes (WAL)

Write-Ahead Log (WAL)

Purpose: Durability (survive crashes) Location: neostore.transaction.db.0, neostore.transaction.db.1, … Process:
Log Entry Format:
Commands:
  • CREATE_NODE: Node ID, labels, properties
  • CREATE_RELATIONSHIP: Rel ID, type, start/end nodes, properties
  • SET_PROPERTY: Node/Rel ID, key, value
  • DELETE_NODE: Node ID
Crash Recovery:
Checkpointing: Periodically, Neo4j flushes in-memory changes to store files and truncates WAL:
Configuration:

Locking

Neo4j uses pessimistic locking (lock before modify). Lock Types:
  1. Read Locks (Shared locks):
    • Multiple transactions can hold read locks simultaneously
    • Prevents writes while reading
  2. Write Locks (Exclusive locks):
    • Only one transaction can hold a write lock
    • Blocks reads and writes
Lock Granularity: Neo4j locks at the node/relationship level:
Deadlock Detection:
Deadlock:
  • TX1 locks Alice, waits for Bob
  • TX2 locks Bob, waits for Alice
  • Deadlock!
Resolution: Neo4j detects deadlock and aborts one transaction:
Best Practice: Always acquire locks in consistent order:

Isolation Levels

Neo4j provides Read Committed isolation by default: Behavior:
  • Reads see committed data only (no dirty reads)
  • Phantom reads possible (another TX inserts/deletes between reads)
Example:
Higher Isolation (Serializable): Neo4j doesn’t support serializable isolation natively. Use explicit locking:

Part 5: Page Cache

Memory Architecture

Page Cache = Neo4j’s in-memory buffer pool (like PostgreSQL’s shared buffers). Purpose:
  • Cache frequently accessed pages (node, relationship, property records)
  • Reduce disk I/O
Configuration:
Sizing Rule: pagecache.size = 0.5 × (RAM - heap - OS) Example (64GB RAM):

Page Cache Structure

Page Size: 8KB (default, configurable) Cache Entry:
Page Eviction: LRU (Least Recently Used) When cache is full:
  1. Find least recently used page
  2. If dirty (modified), flush to disk
  3. Evict page
  4. Load new page into cache

Monitoring Page Cache

Metrics:
  • Hits: Requests served from cache (fast!)
  • Faults: Requests requiring disk read (slow)
  • Evictions: Pages evicted to make room
Hit Rate: hits / (hits + faults) Target: > 95% hit rate Example:
If hit rate < 90%: Increase page cache size!

Part 6: Query Execution

Explain and Profile

Explain (planning only, no execution):
Output:
Profile (execute and collect statistics):
Output:
DB Hits: Number of page cache accesses (lower = better!)

Query Operators

NodeIndexSeek:
NodeByLabelScan:
Expand(All):
Filter:
Sort:
Aggregation (COUNT, SUM, etc.):

Example Query Analysis

Query:
Plan:
Step-by-Step:
  1. NodeIndexSeek: Look up Alice in Person.name index
    • Cost: O(log P)
    • DB Hits: 2
    • Rows: 1
  2. Expand: Traverse :KNOWS relationships
    • Cost: O(D), D = Alice’s degree (e.g., 10 friends)
    • DB Hits: 10 (relationship records)
    • Rows: 10
  3. Filter: Check friend.age > 25
    • Cost: O(10)
    • DB Hits: 10 (read age property)
    • Rows: 6 (assume 6 friends > 25)
  4. Projection: Extract name and age
    • Cost: O(6)
    • DB Hits: 6 (read name property)
    • Rows: 6
  5. Sort: Order by age DESC
    • Cost: O(6 log 6) ≈ 15
    • DB Hits: 0 (in-memory)
    • Rows: 6
  6. ProduceResults: Return to client
    • Rows: 6
Total DB Hits: 2 + 10 + 10 + 6 = 28

Optimization Tips

1. Use Indexes:
2. Filter Early:
3. Limit Results:
4. Use EXPLAIN/PROFILE: Always check query plans for:
  • Missing indexes (NodeByLabelScan instead of NodeIndexSeek)
  • High DB Hits
  • Cartesian products (avoid!)

Part 7: Performance Characteristics

Time Complexity

Typical Values:
  • N = 1,000,000 (total nodes)
  • D = 50 (average degree)
3-hop traversal:
  • Neo4j: 50³ = 125,000
  • Relational: (10⁶)³ = 10¹⁸ (not feasible!)

Benchmark: Social Network Queries

Setup:
  • 1M users
  • 50M friendships (average 50 friends/user)
Query 1: Direct friends
Query 2: Friends of friends
Query 3: Friends up to 3 hops
Key Takeaway: Neo4j shines for deep traversals (2+ hops).

Part 8: Hands-On Exercises

Exercise 1: Explore Store Files

Task: Inspect Neo4j store files and calculate sizes
Questions:
  1. How many nodes? 150M / 15 bytes = 10M nodes
  2. How many relationships? 2.1G / 34 bytes ≈ 61.7M relationships
  3. Average degree? 61.7M × 2 / 10M ≈ 12.3

Exercise 2: Analyze Query Plans

Query:
Tasks:
  1. Identify bottleneck operator (highest DB Hits)
  2. Check if index is used (NodeIndexSeek vs NodeByLabelScan)
  3. Optimize query (add indexes, limit results)

Exercise 3: Measure Page Cache Hit Rate

Expected: Hit rate increases on subsequent runs (data cached)

Summary

Native Graph Storage:
  • Fixed-size records (O(1) access by ID)
  • Pointers for relationships (no JOINs!)
  • Index-free adjacency (O(D) traversal, not O(N))
Indexes:
  • Label indexes (filter by node type)
  • Property indexes (B+tree, O(log N))
  • Full-text indexes (Lucene)
  • Vector indexes (HNSW for ANN)
Transactions:
  • Full ACID guarantees
  • Write-Ahead Log (durability)
  • Pessimistic locking (read/write locks)
  • Deadlock detection
Performance:
  • 10-1000x faster than relational for traversals
  • Scales with degree (D), not graph size (N)
  • Page cache critical for performance (aim > 95% hit rate)

What’s Next?

Module 4: Cypher Query Language Mastery

Master Cypher syntax, pattern matching, aggregations, and advanced query techniques