NoSQL Data Modeling â Document, Key-Value, Wide-Column, and Graph Patterns
In this tutorial, you'll learn about NoSQL Data Modeling. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
NoSQL Data Modeling is the practice of designing data structures for non-relational databases â including document, key-value, wide-column, and graph models â using denormalization, embedded documents, and query-driven schema design instead of normalization and JOINs.
What You'll Learn
You'll understand the four NoSQL data model types, document embedding vs referencing in MongoDB, Cassandra wide-column design, Redis data structures for real-time systems, graph Data Modeling with Neo4j, and how to choose the right NoSQL model for your use case.
Why It Matters
Relational normalization (3NF) works for SQL but fails for NoSQL. Applying SQL Design Patterns to NoSQL databases causes terrible performance. Doda Browser uses multiple NoSQL databases â Redis for session cache, MongoDB for user profiles, Cassandra for event logs â each requiring a different modeling approach.
Real-World Use
A social media startup modeled user profiles in MongoDB using 3NF normalization with separate tables for users, posts, and comments. Each page load required 15 queries. After switching to embedded documents (user profile with recent posts and comments embedded), each page load required 1 query and latency dropped from 800ms to 50ms.
NoSQL Data Modeling Learning Path
flowchart LR A[Database Design] --> B[SQL vs NoSQL] B --> C[NoSQL Data Modeling] C --> D[Graph Databases] C:::current classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Prerequisites: Understanding of Database Design including normalization. Familiarity with MongoDB, Redis, or Cassandra is helpful but not required.
NoSQL Data Model Types
There are four main NoSQL data model families, each optimized for different access patterns.
flowchart TD
NoSQL[NoSQL Databases] --> Document[Document Store
MongoDB, Couchbase]
NoSQL --> KV[Key-Value Store
Redis, DynamoDB]
NoSQL --> WC[Wide-Column Store
Cassandra, HBase]
NoSQL --> Graph[Graph Database
Neo4j, ArangoDB]
Document --> DocPatterns["Embedding, Referencing
Arrays, Polymorphism"]
KV --> KVPatterns["Data structures
TTL, Namespacing"]
WC --> WCPatterns["Partition key, Clustering
Denormalization"]
Graph --> GraphPatterns["Node/Edge model
Traversal patterns"]
Document Data Modeling (MongoDB)
Documents store data as JSON-like structures. The key design decision is embedding vs referencing.
Embedding (Denormalization)
Embed related data inside a single document when it is read together.
// GOOD: Embedding addresses inside user document
// Addresses are always read with the user
db.users.insertOne({
_id: ObjectId("..."),
name: "Alice Smith",
email: "alice@example.com",
addresses: [
{
type: "home",
street: "123 Main St",
city: "Portland",
zip: "97201]
},
{
type: "work",
street: "456 Oak Ave",
city: "Portland",
zip: "97204"
}
],
recent_orders: [
{ order_id: 1001, total: 49.99, date: ISODate("2026-06-20") },
{ order_id: 1002, total: 129.99, date: ISODate("2026-06-21") }
]
});
// One query fetches all user data
db.users.findOne({ email: "alice@example.com" })
Expected output: A single document with user data, addresses, and recent orders. No JOINs needed.
Referencing (Normalization)
Reference other documents when related data is large, frequently updated independently, or accessed separately.
// Using references for large, independently-accessed data
// Order document references the user and product
db.orders.insertOne({
_id: 1001,
user_id: ObjectId("..."), // Reference to user
items: [
{
product_id: ObjectId("..."), // Reference to product
quantity: 2,
price: 24.99
}
],
total: 49.98,
status: "shipped",
created_at: ISODate("2026-06-21")
});
// Application does two queries instead of one JOIN
const order = db.orders.findOne({ _id: 1001 });
const user = db.users.findOne({ _id: order.user_id });
Expected behavior: Data is normalized (no duplication), but reads require multiple queries. This matches SQL patterns but is slower than embedding.
When to Embed vs Reference
| Scenario | Embed | Reference |
|---|---|---|
| Data always read together | Yes | No |
| Data rarely changes | Yes | No |
| Data is small (under 16MB) | Yes | No |
| Data accessed independently | No | Yes |
| Data is large or grows unbounded | No | Yes |
Key-Value Data Modeling (Redis)
Key-value stores are the simplest NoSQL model. The key design decision is key naming convention and data structure choice.
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# Key naming: namespace:entity:id:field
# BAD: Flat keys with no structure
r.set("user_12345", "Alice")
r.set("user_email_12345", "alice@example.com")
# GOOD: Hierarchical key names
r.hset("user:12345", mapping={
"name": "Alice Smith",
"email": "alice@example.com",
"role": "premium"
})
# Store user sessions with TTL (auto-expire)
r.setex("session:abc123", 3600, json.dumps({
"user_id": 12345,
"login_at": "2026-06-22T10:00:00Z",
"ip": "192.168.1.100"
}))
# Use sorted sets for leaderboards
r.zadd("leaderboard:daily", {"user:12345": 1500, "user:67890": 2300})
top_players = r.zrevrange("leaderboard:daily", 0, 9, withscores=True)
print(top_players)
Expected output:
[(b'user:67890', 2300.0), (b'user:12345', 1500.0)]
Redis Data Structure Selection
| Data Structure | Use Case | Example |
|---|---|---|
| String | Cache, counters, simple values | SET user:1:name "Alice" |
| Hash | Object with multiple fields | HSET user:1 name Alice email alice@example.com |
| List | Queue, timeline | LPUSH notifications:1 "New message" |
| Set | Unique members, tags | SADD article:42:tags "database" "nosql" |
| Sorted Set | Leaderboard, rankings | ZADD leaderboard 1500 "user:1" |
Wide-Column Data Modeling (Cassandra)
Wide-column stores like Cassandra use a partition key and clustering columns to define the physical storage layout. The model is query-driven: you design tables for specific queries.
-- Cassandra: Design tables based on queries, not entities
-- Query 1: Get recent messages for a user's inbox
CREATE TABLE messages_by_user (
user_id UUID,
msg_timestamp TIMESTAMP,
sender_id UUID,
subject TEXT,
body TEXT,
is_read BOOLEAN,
PRIMARY KEY ((user_id), msg_timestamp, sender_id)
) WITH CLUSTERING ORDER BY (msg_timestamp DESC, sender_id ASC);
-- This query is efficient because data is partitioned by user_id
-- and clustered by msg_timestamp in DESC order
SELECT * FROM messages_by_user
WHERE user_id = 12345
ORDER BY msg_timestamp DESC
LIMIT 50;
-- Query 2: Get user profiles by email (requires a secondary table)
CREATE TABLE users_by_email (
email TEXT PRIMARY KEY,
user_id UUID,
name TEXT,
created_at TIMESTAMP
);
-- Query 3: Get all users in a specific region
CREATE TABLE users_by_region (
region TEXT,
user_id UUID,
name TEXT,
created_at TIMESTAMP,
PRIMARY KEY ((region), created_at, user_id)
) WITH CLUSTERING ORDER BY (created_at DESC, user_id ASC);
Expected behavior: Each query goes to a specific partition (by user_id, email, or region) and reads a small, sorted set of rows. Full table scans do not exist in Cassandra.
Denormalization in Cassandra
Cassandra encourages data duplication across multiple tables (each optimized for one query pattern).
-- Data is duplicated across tables to support different queries
-- This is NORMAL in Cassandra, not a violation of normalization
-- Table 1: Get orders by user
CREATE TABLE orders_by_user (
user_id UUID,
order_id UUID,
total DECIMAL,
status TEXT,
created_at TIMESTAMP,
PRIMARY KEY ((user_id), created_at, order_id)
) WITH CLUSTERING ORDER BY (created_at DESC);
-- Table 2: Get orders by status (for admin dashboard)
CREATE TABLE orders_by_status (
status TEXT,
created_at TIMESTAMP,
order_id UUID,
user_id UUID,
total DECIMAL,
PRIMARY KEY ((status), created_at, order_id)
) WITH CLUSTERING ORDER BY (created_at DESC);
Expected behavior: Application writes the same order to both tables simultaneously. Reads are fast because each table is designed for its specific query. Storage tradeoff: data duplication for read performance.
Graph Data Modeling (Neo4j)
Graph models focus on nodes (entities) and relationships (connections). See the full Graph Databases tutorial for deep coverage.
// Graph data model for a social e-commerce platform
// Nodes represent entities
CREATE (alice:User {name: 'Alice', joined: date('2025-01-15')});
CREATE (bob:User {name: 'Bob', joined: date('2025-03-20')});
CREATE (product:Product {name: 'Wireless Headphones', price: 79.99});
// Relationships represent connections
CREATE (alice)-[:FOLLOWS]->(bob);
CREATE (alice)-[:PURCHASED {date: date('2026-06-15'), quantity: 1}]->(product);
CREATE (bob)-[:REVIEWED {rating: 5, text: 'Great sound quality!'}]->(product);
// Query: Recommend products that friends of Alice purchased
MATCH (alice:User {name: 'Alice'})-[:FOLLOWS]->(friend:User)
MATCH (friend)-[:PURCHASED]->(product:Product)
WHERE NOT (alice)-[:PURCHASED]->(product)
RETURN product.name, COUNT(*) AS popularity
ORDER BY popularity DESC
LIMIT 5;
Expected output:
product_name | popularity
-------------------+------------
Wireless Headphones | 3
Bluetooth Speaker | 2
Choosing the Right NoSQL Model
| Your Need | Best Model | Example Database |
|---|---|---|
| Flexible schemas, nested data | Document | MongoDB |
| Simple key lookups, Caching | Key-Value | Redis |
| High write throughput, time-series | Wide-Column | Cassandra |
| Connected data, relationships | Graph | Neo4j |
| Full-text search | Document/Search | Elasticsearch |
Common NoSQL Data Modeling Errors
1. Applying 3NF Normalization to NoSQL
Normalizing data in MongoDB (separate collections, multiple JOIN-like queries) ignores the document model's strength. Embed related data unless there's a specific reason not to.
2. Creating Unbounded Arrays (MongoDB)
Embedding an unlimited number of child documents (like all chat messages for a user) can exceed MongoDB's 16MB document size limit.
// BAD: Unbounded embedding in MongoDB
db.users.updateOne(
{ _id: user_id },
{ $push: { messages: { text: "hello", timestamp: new Date() } } }
);
// Over time, this array grows without bound â 16MB limit hit
// GOOD: Store messages in a separate collection
db.messages.insertOne({
user_id: user_id,
text: "hello",
timestamp: new Date()
});
3. Choosing the Wrong Cassandra Partition Key
A bad partition key creates hot spots (all data in one partition) or wide partitions (millions of rows in one partition).
-- BAD: Low cardinality partition key
CREATE TABLE events_by_type (
event_type TEXT, -- Only 10 distinct values
timestamp TIMESTAMP,
data TEXT,
PRIMARY KEY ((event_type), timestamp)
);
-- The 'click' partition gets 80% of writes â HOT SPOT
-- GOOD: High cardinality partition key
CREATE TABLE events_by_user (
user_id UUID, -- Millions of distinct values
timestamp TIMESTAMP,
event_type TEXT,
data TEXT,
PRIMARY KEY ((user_id), timestamp)
);
4. Forgetting TTL for Ephemeral Data
Session data, temporary tokens, and cache entries need automatic expiration. Redis and Cassandra support TTL natively.
# Redis: Auto-expire session data
r.setex("session:abc", 3600, session_data) # Expires in 1 hour
# Cassandra: TTL on insert
INSERT INTO user_sessions (session_id, user_id, data)
VALUES ('abc123', 12345, '{"ip": "10.0.0.1"}')
USING TTL 86400; -- Expires in 24 hours
5. Ignoring Read/Write Ratio
A read-heavy application needs different modeling than a write-heavy one. Cassandra excels at writes but has slower reads. Redis excels at reads but is memory-bound for writes.
6. Not Designing for Query Patterns
NoSQL databases require you to know your queries before designing the schema. You cannot add an index later to support a new query pattern (especially in Cassandra).
7. Mixing Workload Types in One Database
Using the same MongoDB collection for transactional data (need ACID) and analytics (need aggregation) causes poor performance for both. Separate workloads into different databases.
Practice Questions
1. What is the main difference between Data Modeling in SQL vs NoSQL?
SQL uses normalization: separate tables for each entity, connected by JOINs. NoSQL uses query-driven design: structure data to match access patterns, often denormalizing and duplicating data to avoid JOINs.
2. When should you embed documents in MongoDB vs reference them?
Embed when related data is always read together, is small, and rarely changes independently. Reference when data is large, grows unbounded, or is accessed separately.
3. Why does Cassandra require designing tables for specific queries?
Cassandra's storage engine sorts data on disk by the clustering columns. If a query doesn't filter on the partition key or uses a different sort order, Cassandra cannot execute it efficiently. You must create a separate table for each query pattern.
4. What Redis data structure should you use for a real-time leaderboard?
Sorted Set (ZSET). Scores are updated with ZADD, top players are retrieved with ZREVRANGE, and ranking is checked with ZRANK â all O(log N) operations.
5. Challenge: Design a NoSQL data model for a real-time chat application.
Your chat app has 10 million users, each with multiple conversations. Messages must be delivered in order and stored for 30 days. Users can search their message history. Answer: Use Cassandra for message storage: messages_by_conversation(conversation_id, timestamp, sender_id, text, PRIMARY KEY ((conversation_id), timestamp)). Use Redis for online status and typing indicators (Hash with TTL). Use Elasticsearch for message search. Use Redis Pub/Sub for real-time delivery. Messages older than 30 days are deleted via Cassandra TTL.
FAQ
Try It Yourself
Model the same application across different NoSQL databases:
- Design a blog platform with users, posts, comments, and tags
- Model it in MongoDB: decide what to embed vs reference
- Model it in Cassandra: design tables for each query pattern
- Model it in Redis: decide on key naming and data structures
- Compare the models: which is simplest? fastest? most scalable?
- Write example queries in each database's query language
What's Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro