Executive Architectural Overview#
Modern data architectures frequently encounter workloads that lie outside the optimal performance profiles of relational and document databases:
- High-Velocity Append Streams: Ingestion engines processing millions of time-series telemetry events or financial audit logs per second, where random disk updates and relational locking throttle write throughput.
- Dense Relationship Topologies: Social networks, cybersecurity threat graphs, financial fraud rings, and master data knowledge graphs, where entities are bound by multi-hop connections. In relational databases, traversing K relationship hops requires K-way recursive joins, triggering exponential runtime explosion (O(B^K) where B is the branching factor).
To resolve these orthogonal engineering challenges, the NoSQL paradigm developed two specialized architectures:
- Wide-Column / Column-Family Stores (exemplified by Apache Cassandra and ScyllaDB): Designed for peer-to-peer linear write scalability, storing data in Log-Structured Merge (LSM) trees across decentralized consistent hashing rings.
- Graph Databases (exemplified by Neo4j): Designed for relationship-first processing, implementing Index-Free Adjacency where relationship pointers are stored directly on disk blocks, enabling constant-time O(1) edge traversal.
SPECIALIZED DATA STORE TAXONOMY
┌────────────────────────────────────────────────────────┐
│ WIDE-COLUMN STORES (Cassandra, ScyllaDB) │
│ • Log-Structured Merge (LSM) Trees │
│ • Masterless Peer-to-Peer Ring (Zero Single Point) │
│ • Write-optimized: In-memory MemTable + Append Commit │
│ • Query-Driven Modeling: One table per query pattern │
└──────────────────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ GRAPH DATABASES (Neo4j, Amazon Neptune) │
│ • Index-Free Adjacency (Double-linked pointer chains) │
│ • Labeled Property Graph (LPG) Model │
│ • Constant-time O(1) Relationship Traversal │
│ • Declarative pattern matching with Cypher │
└────────────────────────────────────────────────────────┘1. Wide-Column Stores: Apache Cassandra & ScyllaDB Architecture#
Apache Cassandra and its high-performance C++ rewrite ScyllaDB implement a decentralized, leaderless shared-nothing architecture based on the Amazon Dynamo distribution model and Google Bigtable storage layout.
The Peer-to-Peer Decentralized Ring#
Unlike primary-replica systems (e.g., MongoDB or PostgreSQL) where all writes funnel through a single master coordinator:
- In Cassandra, every node is architecturally identical (peer-to-peer).
- A client can connect to any arbitrary node in the cluster (acting as the Coordinator).
- The coordinator hashes the row's partition key using the Murmur3Partitioner:
Token = Murmur3Hash(partition_key) ∈ [-2^{63}, 2^{63} - 1]- The coordinator inspects the cluster token ring and forwards the write directly to the N replica nodes responsible for that token range.
CASSANDRA CONSISTENT HASH RING
──────────────────────────────
Node A (Token: 0)
/ \
/ \
Node F (-2^62) Node B (2^62)
| |
| 128 Virtual Nodes |
| (vnodes) per Physical |
Node E (-2^63) Node C (2^63 - 1)
\ /
\ /
Node D (Token: ~0)Virtual Nodes (vnodes):
Instead of assigning a single contiguous token range to a physical server, modern Cassandra clusters assign each physical machine 128 or 256 virtual nodes scattered randomly across the 64-bit token ring. When a physical node crashes or a new server is added, data is streamed uniformly across dozens of peer nodes simultaneously, preventing I/O hotspots and drastically accelerating rebalance times.
2. The LSM-Tree Storage Engine: Write and Read Paths#
Traditional relational databases update data in-place on disk B-Tree pages. An in-place update requires random disk I/O, which incurs high latency and causes solid-state write amplification. Cassandra achieves ultra-high write throughput by treating all writes as immutable sequential appends using Log-Structured Merge (LSM) Trees.
CASSANDRA PHYSICAL WRITE PATH
─────────────────────────────
Incoming Client Write
│
┌─────────────────────────┴─────────────────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ CommitLog │ (Sequential Disk Append) │ MemTable │ (In-Memory SkipList)
│ Durability WAL│ Fast sequential I/O │ Concurrent │ Fast atomic update
└───────────────┘ └───────┬───────┘
│
Flushed when full (100% capacity)
▼
┌───────────────┐
│ SSTable │ (Immutable Disk File)
│ Data, Filter, │ Ordered by (Token,
│ Summary, Index│ Clustering Key)
└───────────────┘The Write Path (Sequential Zero-Lock Append)#
When a write arrives at a replica node:
- CommitLog Append: The write appends to an on-disk sequential log file (the CommitLog). Because sequential disk writes achieve peak SSD/NVMe throughput without head seeks, this takes microseconds.
- MemTable Insertion: Simultaneously, the write is inserted into the MemTable, an in-memory data structure (typically a concurrent SkipList) that keeps rows sorted by partition token and clustering columns.
- Acknowledgment: Once written to the CommitLog and MemTable, the replica immediately returns a success acknowledgment to the coordinator. No table locks or B-Tree page rewrites occur!
- SSTable Flush: When the MemTable reaches its capacity threshold (or flush timer expires), it is flushed sequentially to persistent storage as an immutable SSTable (Sorted String Table).
The Read Path Internals#
Because writes never update old data in-place, the latest version of a record might reside in the active MemTable, an unflushed MemTable, or scattered across dozens of on-disk SSTables. To satisfy a read request efficiently, Cassandra executes a highly optimized pipeline:
CASSANDRA COMPREHENSIVE READ PIPELINE
─────────────────────────────────────
Client Read Request
│
▼
Check Active MemTable
│ (If not fully satisfied)
▼
Key Cache (RAM Lookup)
│ (Cache Miss)
▼
┌─────────────────────────────┐
│ Bloom Filter Check (RAM) │
└──────────────┬──────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
Returns FALSE (99% certainty) Returns TRUE (Candidate)
│ │
SKIP SSTable entirely! ▼
(Zero Disk I/O performed) Partition Summary (RAM)
│
▼
Partition Index (OS Page Cache)
│
▼
SSTable Data File (Disk Seek)
│
▼
Merge Multi-SSTable Rows
(Highest Timestamp Wins!)- Bloom Filter Check: Every SSTable has an associated Bloom filter residing in RAM. The Bloom filter determines with 99% accuracy whether an SSTable definitely does not contain the requested partition key. If false, Cassandra skips the SSTable completely, eliminating disk seeks.
- Partition Summary & Index: If the Bloom filter returns true, the engine consults the in-memory Partition Summary, which points to an offset in the on-disk Partition Index, which in turn points to the exact byte offset in the SSTable data file.
- Row Merge & Tombstones: Cassandra reads matching fragments from the MemTable and candidate SSTables, merges them by timestamp, and returns the freshest value.
3. Compaction Strategies & The Tombstone Hazard#
Because SSTables are immutable, updates and deletes do not overwrite disk blocks:
- Updates append a new version of the row with a higher timestamp.
- Deletes append a special marker called a Tombstone recording the deletion timestamp.
Over time, hundreds of SSTables accumulate on disk. Compaction is the background subsystem that reads multiple old SSTables, reconciles versions, purges expired tombstones, and writes out a single consolidated, sorted SSTable.
+─────────────────────+───────────────────────────────────────+────────────────────────+
| Compaction Strategy | Operational Mechanics | Ideal Workloads |
+─────────────────────+───────────────────────────────────────+────────────────────────+
| Size-Tiered (STCS) | Triggers when 4 SSTables of similar | Write-heavy general; |
| | size exist; merges them into larger. | requires 50% free disk!|
+─────────────────────+───────────────────────────────────────+────────────────────────+
| Leveled (LCS) | Divides SSTables into geometric levels| Read-heavy workloads; |
| | (L0, L1, L2...); 90% of reads hit 1 | predictable latency; |
| | SSTable; requires 10% disk headroom. | higher write overhead. |
+─────────────────────+───────────────────────────────────────+────────────────────────+
| Time Window (TWCS) | Buckets SSTables by time window; | Time-series & IoT data |
| | never merges across time boundaries; | with TTL; drops entire |
| | drops entire files upon TTL expiry. | SSTables instantly! |
+─────────────────────+───────────────────────────────────────+────────────────────────+The Tombstone Hazard & Read Timeouts#
If an application frequently deletes records or inserts NULL values into columns, millions of tombstones accumulate in the SSTables.
When a query requests data with a range scan:
- Cassandra must read and iterate through every consecutive tombstone to check if it masks underlying data.
- If the query encounters more than
tombstone_warn_threshold(default 1,000), it logs severe warnings. - If it encounters more than
tombstone_failure_threshold(default 100,000 tombstones), Cassandra aborts the query with a ReadTimeoutException to protect JVM heap memory from out-of-memory crashes.
> Production Rule: Never use Cassandra as a temporary work queue with rapid insert-and-delete cycles. For queue semantics, use Redis or RabbitMQ. In Cassandra, manage lifecycle via TTL coupled with the Time Window Compaction Strategy (TWCS).
4. Cassandra CQL Data Modeling Principles#
In relational database design, you model data around Entities and Relationships (ER diagrams), normalizing tables to Third Normal Form, relying on SQL joins to answer arbitrary future queries.
In Cassandra, there are no joins and no aggregations across partitions. You must model data strictly around your Application Queries.
The Anatomy of a Cassandra Primary Key#
A Cassandra Primary Key consists of two distinct components:
PRIMARY KEY ((partition_key_columns), clustering_columns)- Partition Key:
- Determines which physical node(s) in the cluster store the row via Murmur3 token hashing.
- All rows sharing the identical partition key reside on the same physical server node.
- Can be compound: PRIMARY KEY ((tenant_id, sensor_type), recorded_at).
- Clustering Columns:
- Dictates the physical sort order of rows inside that specific partition on disk.
- Enables ultra-fast range queries (>, <, BETWEEN) within a single partition.
-- Production Sensor Table
CREATE TABLE sensor_telemetry (
fleet_id UUID,
truck_id UUID,
event_date DATE,
logged_at TIMESTAMP,
latitude DOUBLE,
longitude DOUBLE,
speed_kmh FLOAT,
PRIMARY KEY ((fleet_id, event_date), truck_id, logged_at)
) WITH CLUSTERING ORDER BY (truck_id ASC, logged_at DESC);- Partition Key:
(fleet_id, event_date)guarantees all telemetry for a fleet on a single day lands on the same cluster node. - Clustering Columns:
truck_idandlogged_atensure that within that day's partition, records are physically stored pre-sorted by truck and in descending chronological order.
5. Graph Databases & Neo4j: Index-Free Adjacency#
While wide-column stores excel at high-throughput tabular partitioning, they struggle with deeply interconnected networks. In a relational database, traversing relationships between users, products, and transactions requires foreign key B-Tree index lookups for every hop.
The Index-Free Adjacency Paradigm#
Neo4j pioneers Index-Free Adjacency:
- In a traditional database, an edge between Node A and Node B is resolved by searching a secondary index: O(log N) complexity per hop. Across K hops, query time grows exponentially with total database size.
- In Neo4j, nodes and relationships are stored as fixed-size records on disk containing direct physical double-linked memory/disk pointers to adjacent records:
Cost per Hop = O(1) (Constant Time!)Traversing a relationship in Neo4j does not involve an index lookup; the storage engine simply dereferences the memory pointer directly to the adjacent node. Query performance is determined strictly by the size of the subgraph being traversed, completely independent of whether the total database contains 10,000 nodes or 10,000,000,000 nodes.
INDEX-BASED JOIN (RELATIONAL) INDEX-FREE ADJACENCY (NEO4J)
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ Table A: Node 1 │ │ [Node Record A] │
│ │ │ │ first_relationship_ptr ────────┼─┐
│ ▼ │ └──────────────────────────────────┘ │
│ Index B-Tree Lookup: O(log N) │ │
│ │ │ ┌──────────────────────────────────┐ │
│ ▼ │ │ [Relationship Record R1] ◄───────┘
│ Table B: Node 2 │ │ next_rel_ptr, target_node_ptr ─┼─┐
│ (Cost scales with Table Size!) │ └──────────────────────────────────┘ │
└──────────────────────────────────┘ ▼
┌──────────────────────────────────┐
│ [Node Record B] ◄────────────────┘
│ (Direct Pointer Dereference: O(1))
└──────────────────────────────────┘The Labeled Property Graph (LPG) Model#
The core abstraction of Neo4j is the Labeled Property Graph:
- Nodes: Discrete domain entities (e.g.,
:Person,:Account,:Device). Nodes can hold multiple labels. - Relationships: Directed, typed connections between two nodes (e.g.,
-[:TRANSFERRED_FUNDS]->,-[:OWNS_DEVICE]->). A relationship must always have a start node and an end node. - Properties: Key-value pairs stored directly on nodes and on relationships (e.g., a
:TRANSFERRED_FUNDSrelationship can hold properties likeamount: 5000.00andtimestamp: "2026-10-30T10:00:00Z").
Cypher Query Language: Declarative Pattern Matching#
Neo4j introduces Cypher, an intuitive ASCII-art pattern matching query language:
// Discover 2nd-degree friends who like the same music genre
MATCH (u:Person {username: "Alice"})-[:FRIENDS_WITH]->(f:Person)-[:FRIENDS_WITH]->(fof:Person)
MATCH (fof)-[:LIKES_GENRE]->(g:Genre)
WHERE NOT (u)-[:FRIENDS_WITH]->(fof) AND u <> fof
RETURN fof.username AS recommended_friend, g.name AS shared_genre, COUNT(*) AS affinity_score
ORDER BY affinity_score DESC
LIMIT 10;6. Comprehensive Technology Decision Matrix#
+──────────────+──────────────────────────+─────────────────────────+─────────────────────────+
| Attribute | Relational (PostgreSQL) | Wide-Column (Cassandra) | Graph Database (Neo4j) |
+──────────────+──────────────────────────+─────────────────────────+─────────────────────────+
| Primary Data | Normalized 2D Tables | Multi-dimensional map: | Nodes, Relationships, |
| Model | with Foreign Keys. | (Partition, Cluster). | Properties (LPG). |
+──────────────+──────────────────────────+─────────────────────────+─────────────────────────+
| Write Scaling| Vertical scale-up; | Linear horizontal scale;| Vertical scale-up; read |
| Profile | master-replica writes. | masterless peer ring. | clustering replicas. |
+──────────────+──────────────────────────+─────────────────────────+─────────────────────────+
| Read Profile | Fast for normalized rows;| Ultra-fast single-key | Fast for complex graph |
| | slow for deep joins. | partition reads. | path traversals. |
+──────────────+──────────────────────────+─────────────────────────+─────────────────────────+
| Traversal | O(B^K) via recursive | Anti-pattern; requires | O(1) per hop via |
| Complexity | join index seeks. | manual multi-queries. | pointer dereference. |
+──────────────+──────────────────────────+─────────────────────────+─────────────────────────+
| Best Fit | ERP, transactional ACID, | High-throughput time- | Fraud rings, social |
| Workload | ad-hoc reporting. | series, logging, chats. | networks, dependencies. |
+──────────────+──────────────────────────+─────────────────────────+─────────────────────────+7. Enterprise Checkpoint Practice Problems#
Test your mastery with these four real-world production checkpoint problems. Each problem presents a concrete business schema, data samples, edge cases, a failed attempt analysis, and a fully explained optimal solution.
Problem 1: Cassandra IoT Fleet Table Modeling with TWCS & Tombstone Immunity#
Context: An enterprise energy company monitors 100,000 smart grid meters.
- Each meter emits voltage, frequency, and kilowatt consumption every 10 seconds.
- The query pattern is strictly: "Fetch the last 4 hours of readings for a specific
meter_id, ordered chronologically". - Data must be retained for exactly 30 days and automatically deleted.
- Naive implementations suffered from massive tombstone-induced read timeouts after 2 weeks of operation.
- Model the optimal CQL table definition, specify the clustering order, configure the correct Compaction Strategy, and explain how tombstones are avoided.
Optimal Production CQL Implementation:
CREATE KEYSPACE smart_grid_telemetry
WITH replication = {
'class': 'NetworkTopologyStrategy',
'us-east-1': 3,
'us-west-2': 3
};
USE smart_grid_telemetry;
CREATE TABLE meter_readings_by_day (
meter_id UUID,
reading_date DATE,
logged_at TIMESTAMP,
voltage_v FLOAT,
frequency_hz FLOAT,
active_power_kw FLOAT,
PRIMARY KEY ((meter_id, reading_date), logged_at)
) WITH CLUSTERING ORDER BY (logged_at DESC)
AND default_time_to_live = 2592000 -- 30 days in seconds
AND compaction = {
'class': 'TimeWindowCompactionStrategy',
'compaction_window_unit': 'DAYS',
'compaction_window_size': '1',
'timestamp_resolution': 'MILLISECONDS'
};Architectural Walkthrough:
- Partition Key `(meter_id, reading_date)`: Bounding the partition key by date guarantees that partitions remain small (approx. 8,640 rows per meter per day ≈ 500 KB), completely preventing oversized partitions.
- Clustering Order `(logged_at DESC)`: Readings are physically stored newest-first on disk, allowing the query fetching the last 4 hours to read sequential blocks from the head of the file and terminate early.
- TimeWindowCompactionStrategy (TWCS): Groups SSTables into 1-day windows. Because all data in an SSTable shares a 30-day TTL, when day 31 arrives, the entire SSTable expires simultaneously. Cassandra drops the entire physical file from disk instantly via an OS file unlink, generating zero tombstones!
Problem 2: Neo4j Credit Card Fraud Ring Detection & Pattern Matching#
Context: A banking security team investigates synthetic identity fraud. Fraud rings create synthetic personas by sharing phone numbers, home addresses, and credit cards across multiple user accounts.
- Write a Cypher query to detect fraud rings where 3 or more distinct user accounts share either the same home address, phone number, or government tax identifier.
- Calculate the total transactional credit volume stolen by the ring and return the suspicious cluster graph.
Graph Schema:
- Nodes:
(:User {id, name, credit_limit}),(:Address {street, city}),(:Phone {number}) - Relationships:
[:LIVES_AT],[:HAS_PHONE]
Optimal Production Cypher Solution:
// Step 1: Match users connected through shared contact identifiers
MATCH (u:User)-[:LIVES_AT|HAS_PHONE]->(shared_node)<-[:LIVES_AT|HAS_PHONE]-(other:User)
WHERE u.id < other.id // Prevent bidirectional duplicate pairs
WITH shared_node, collect(DISTINCT u) + collect(DISTINCT other) AS connected_users
UNWIND connected_users AS user
WITH shared_node, collect(DISTINCT user) AS ring_members
WHERE size(ring_members) >= 3
// Step 2: Compute risk metrics across the detected fraud ring
UNWIND ring_members AS suspect
MATCH (suspect)-[r:PERFORMED_TRANSACTION]->(t:Transaction)
WHERE t.timestamp >= datetime() - duration('P30D')
RETURN
shared_node.number AS shared_contact_identifier,
labels(shared_node)[0] AS identifier_type,
[m IN ring_members | m.name] AS ring_suspect_names,
size(ring_members) AS total_suspect_count,
COUNT(t) AS total_transactions_30d,
SUM(t.amount) AS total_stolen_volume_usd
ORDER BY total_stolen_volume_usd DESC
LIMIT 20;Architectural Walkthrough:
[:LIVES_AT|HAS_PHONE]utilizes multi-relationship type pattern matching in a single traversal pass.WHERE u.id < other.idprunes symmetric duplicate paths (O(N^2) reduction).Index-Free Adjacencytraverses between user and contact nodes in microseconds without searching external indexes.
Problem 3: Social Network Friend-of-Friend Recommendation with Degree-Bounded BFS#
Context: A social network wants to generate friend recommendations ("People You May Know").
- Find friends-of-friends (2 hops away).
- Rank candidates by the number of mutual friends shared with the root user.
- High-degree celebrity nodes (influencers with 2,000,000 followers) cause memory exhaustion and combinatorial explosions if traversed.
- Implement degree-bounded path filtering in Cypher to prune celebrity supernodes.
Optimal Production Cypher Solution:
MATCH (root:User {username: "kevin_b"})
// Step 1: Match direct friends who are NOT supernodes (degree < 5,000)
MATCH (root)-[:FRIEND]->(f:User)
WHERE size((f)-[:FRIEND]-()) < 5000
// Step 2: Traverse to 2nd-degree friends
MATCH (f)-[:FRIEND]->(candidate:User)
WHERE candidate <> root
AND NOT (root)-[:FRIEND]->(candidate)
AND size((candidate)-[:FRIEND]-()) < 5000
// Step 3: Aggregate mutual connections
RETURN
candidate.username AS recommended_user,
COUNT(DISTINCT f) AS mutual_friend_count,
collect(DISTINCT f.username)[0..5] AS sample_mutual_friends
ORDER BY mutual_friend_count DESC
LIMIT 15;Architectural Walkthrough:
size((f)-[:FRIEND]-()) < 5000evaluates node degree from the node's record header in O(1) time without reading edge lists.- Pruning celebrity supernodes prevents exponential branching factor blowups (B^2).
- Returns top candidates ordered by mutual connection density.
Problem 4: Cassandra Partition Key Sharding & Hotspot Elimination via Synthetic Salt Bucketing#
Context: A global video streaming platform logs video playback events (video_id, user_id, watch_time_sec, timestamp).
- A viral video accumulates 50,000,000 views in 24 hours.
- If partitioned simply by
video_id, all 50M views land on a single Cassandra partition key. - The partition size exceeds 2 Gigabytes (violating Cassandra's 100MB maximum partition size best practice) and saturates the single replica node hosting that token.
- Design a Synthetic Salt Bucketing strategy that distributes the writes uniformly across 100 shards while maintaining the ability to read all views for that video.
Optimal Production Strategy & Table Model:
CREATE TABLE video_views_by_bucket (
video_id UUID,
bucket_id INT, -- Salt bucket: 0 to 99
view_timestamp TIMESTAMP,
user_id UUID,
watch_time_sec INT,
device_type VARCHAR,
PRIMARY KEY ((video_id, bucket_id), view_timestamp, user_id)
) WITH CLUSTERING ORDER BY (view_timestamp DESC, user_id ASC);Application Layer Routing Logic (Python Driver):
import uuid
import random
from cassandra.cluster import Cluster
cluster = Cluster(['10.0.0.1', '10.0.0.2'])
session = cluster.connect('streaming_telemetry')
NUM_BUCKETS = 100
def record_video_view(video_id: uuid.UUID, user_id: uuid.UUID, watch_time: int, device: str):
# Salt incoming write with uniform random bucket: 0 to 99
bucket_id = random.randint(0, NUM_BUCKETS - 1)
query = '''
INSERT INTO video_views_by_bucket
(video_id, bucket_id, view_timestamp, user_id, watch_time_sec, device_type)
VALUES (%s, %s, toTimestamp(now()), %s, %s, %s)
'''
session.execute(query, (video_id, bucket_id, user_id, watch_time, device))
def read_latest_video_views(video_id: uuid.UUID, limit_per_bucket=10):
# Query all 100 buckets concurrently via asynchronous futures
query = '''
SELECT view_timestamp, user_id, watch_time_sec, device_type
FROM video_views_by_bucket
WHERE video_id = %s AND bucket_id = %s
LIMIT %s
'''
futures = [session.execute_async(query, (video_id, b, limit_per_bucket)) for b in range(NUM_BUCKETS)]
# Merge-sort results in application memory
all_views = []
for f in futures:
all_views.extend(f.result())
all_views.sort(key=lambda x: x.view_timestamp, reverse=True)
return all_views[:100]Architectural Walkthrough:
- Synthetic bucketing (
bucket_id = random(0, 99)) splits the viral video's token hashes across 100 different token ranges on the ring. - The 2 GB partition is broken down into 100 balanced 20MB partitions, safely within Cassandra's 100MB limit.
- Write throughput scales linearly across the entire cluster without hotspotting.
Conclusion & Architectural Summary#
- LSM-Tree Supremacy: Wide-column stores append writes sequentially to CommitLogs and MemTables, flushing immutable SSTables to deliver high write throughput.
- Query-Driven Cassandra Modeling: Tables must be designed around specific query access patterns; partition keys govern cluster placement, while clustering columns determine on-disk sort order.
- Tombstone Discipline: Avoid rapid delete patterns in Cassandra to prevent tombstone accumulation; manage data lifecycles with TTLs and the Time Window Compaction Strategy (TWCS).
- Index-Free Adjacency: Graph databases like Neo4j store direct pointers on disk between connected records, delivering constant-time O(1) relationship traversal independent of total graph scale.
- Architectural Selection: Select wide-column stores for high-throughput time-series and write-heavy workloads; choose graph databases for complex, multi-hop relationship queries.