NoSQL Data Modeling
How to model data in NoSQL databases using denormalization, access-pattern-driven design, and practical patterns for document, wide-column, and key-value.
Quick answer: NoSQL Data Modeling
NoSQL Data Modeling is covered here as a practical system design concept. How to model data in NoSQL databases using denormalization, access-pattern-driven design, and practical patterns for document, wide-column, and key-value. Use it to understand the Databases idea, where it appears in production systems, and how to discuss it clearly in interviews.
Focus on the problem being solved, the main design choice, the failure mode, and the tradeoff you would defend in front of a senior engineer. This page is part of the DosiBridge system design knowledge graph, so it links related concepts, interview problems, tradeoffs, and references instead of leaving the topic isolated.
Why NoSQL Modeling Is Fundamentally Different
Relational data modeling starts with the data. You normalize it into tables, define relationships, and trust that the query optimizer will figure out how to join things efficiently. You model first, query later.
NoSQL data modeling starts with the queries. You figure out exactly how your application will access the data, and then you design the data model to serve those access patterns directly. You query first, model later.
This inversion trips up every engineer coming from a relational background. If you try to normalize data in DynamoDB or Cassandra the way you would in PostgreSQL, you will end up with a system that performs terribly and costs a fortune. Rick Houlihan, the principal technologist behind DynamoDB, has a famous line: "If you are doing joins in a NoSQL database, you are doing it wrong."
Access Pattern Driven Design
The first step in NoSQL modeling is to enumerate every access pattern your application needs. Write them down explicitly:
- Get user profile by user ID
- Get all orders for a user, sorted by date
- Get all items in an order
- Get the 20 most recent posts in a feed
- Get all comments on a post
Each access pattern maps to a specific query. In a relational database, you might serve all of these from normalized tables with joins. In a NoSQL database, each access pattern might require its own data arrangement — either a different table, a different partition key, or a Global Secondary Index.
At Amazon, teams building on DynamoDB go through a formal access pattern exercise before writing a single line of code. They list every read and write operation, the expected frequency, the latency requirement, and the data involved. Only then do they design the table schema.
Denormalization: Embracing Data Duplication
In relational modeling, duplication is a sin. Third Normal Form exists to eliminate it. In NoSQL modeling, duplication is a feature.
Consider an e-commerce order. In a relational database, you would have an orders table, an order_items table, a products table, and a users table. To display an order confirmation, you join all four tables.
In DynamoDB, you would store the entire order as a single item: the order metadata, the line items (with product name and price embedded), and the user's shipping address — all in one document. Yes, the product name is duplicated. Yes, the shipping address is duplicated. But the read is a single-digit-millisecond key lookup instead of a multi-table join.
The tradeoff is clear: you trade storage efficiency and write complexity for read performance. When the product name changes, you may need to update it in every order that references it — or you accept that historical orders show the name at the time of purchase (which is often the correct business behavior anyway).
Single Table Design in DynamoDB
The most powerful (and most misunderstood) NoSQL modeling technique is single table design. Instead of creating multiple tables for different entity types, you store everything in one table with a carefully designed partition key and sort key.
For example, a social media application might use:
PK | SK | Data
USER#123 | PROFILE | {name, email, bio}
USER#123 | POST#2024-01-15 | {title, body, likes}
USER#123 | POST#2024-01-10 | {title, body, likes}
USER#123 | FOLLOWER#456 | {followedAt}
POST#2024-01-15 | COMMENT#001 | {author, text}
With this design:
- Get user profile: query
PK = USER#123, SK = PROFILE - Get user's posts: query
PK = USER#123, SK begins_with POST# - Get comments on a post: query
PK = POST#2024-01-15, SK begins_with COMMENT#
One table. Zero joins. Every query is a single partition lookup. This is how DynamoDB was designed to be used, and it is how Amazon's own services use it internally.
The downside is that the data model is harder to understand at a glance. You lose the clarity of separate tables with descriptive names. New team members need to understand the key structure to make sense of the data. This is a real cost, and for many teams, a multi-table design with clear naming is the better choice.
Wide-Column Modeling in Cassandra
Cassandra's data model revolves around partitions. Each partition is identified by a partition key and contains rows sorted by clustering columns. The cardinal rule: design your partition key so that each query hits exactly one partition.
For a messaging application:
CREATE TABLE messages (
channel_id UUID,
message_id TIMEUUID,
sender_id UUID,
body TEXT,
PRIMARY KEY (channel_id, message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);
This design serves the primary access pattern (get recent messages in a channel) with a single partition read. Messages are pre-sorted by time within each partition, so "get the 50 most recent messages" requires no sorting at query time.
Discord uses exactly this pattern. Their partition key is (channel_id, bucket) where the bucket is a time window. The bucket prevents partitions from growing unbounded — a critical concern because Cassandra partitions that exceed a few hundred megabytes cause compaction problems and read latency spikes.
Document Modeling in MongoDB
MongoDB's document model maps naturally to object-oriented code. The key modeling decision is whether to embed related data within a document or reference it via an ID.
Embed when:
- The related data is always accessed together with the parent
- The related data has a bounded size (a user's shipping addresses, not a user's entire order history)
- The data has a one-to-few relationship
Reference when:
- The related data is accessed independently
- The related data is unbounded (comments on a popular post)
- Multiple parent documents reference the same child
Instagram's early MongoDB usage embedded comments within post documents. This worked until viral posts accumulated thousands of comments, causing documents to exceed MongoDB's 16MB limit and degrading read performance. They eventually moved to PostgreSQL.
Handling Relationships Without Joins
NoSQL databases do not support joins (with rare exceptions like DocumentDB's $lookup). When your application needs data from multiple entities, you have three options:
Denormalize at write time. Duplicate the data so that each query can be served from a single read. This is the most common approach. When a user changes their display name, update it everywhere it appears.
Aggregate at application level. Issue multiple queries and combine the results in your application code. This works when the number of queries is small and predictable. Fetching a user profile and their 5 most recent posts requires two queries, which is acceptable.
Use materialized views or change data capture. When a write occurs, trigger a process that updates pre-computed views. Cassandra has built-in materialized views (though they have operational limitations). DynamoDB Streams combined with Lambda functions can maintain derived data asynchronously.
Anti-Patterns to Avoid
Treating NoSQL like SQL. Creating a table per entity type with foreign key-like references and then doing multiple queries to "join" them in the application. You get the worst of both worlds: no join optimization and the complexity of distributed data.
Unbounded partition growth. A partition key like country_code means all users in the United States land in one partition. That partition grows without limit and becomes a hot spot. Always bound your partitions — add a time bucket, a shard suffix, or a composite key.
Scanning instead of querying. If your queries require a full table scan, your data model is wrong. Every query should map to a specific partition key lookup or a range query within a partition.
Premature optimization with single table design. Single table design is powerful but adds cognitive overhead. If you have 3-5 entity types with straightforward access patterns, separate tables are easier to reason about and maintain. Reserve single table design for cases where you have many entity types with overlapping access patterns.
When to Choose Which NoSQL Model
Key-value (Redis, DynamoDB simple mode): Session storage, feature flags, user preferences. The access pattern is always "get by key."
Document (MongoDB, Firestore): Content management, user profiles, product catalogs. The data is hierarchical and accessed as a unit.
Wide-column (Cassandra, ScyllaDB): Time-series data, messaging, IoT sensor data. The access pattern involves range queries within a partition.
Graph (Neo4j, Neptune): Social networks, recommendation engines, fraud detection. The queries traverse relationships between entities.
The data model you choose constrains everything else. Get it right, and your system will be fast and cheap to operate. Get it wrong, and no amount of infrastructure will save you.
Real-World Production Example
When Lyft built their rider and driver matching system on DynamoDB, they needed to model real-time location data for millions of active drivers. The primary access pattern was "find all available drivers within a geographic region," which is not a pattern DynamoDB natively supports — DynamoDB excels at key-value lookups, not spatial queries.
Lyft solved this by using geohashing to convert two-dimensional coordinates into one-dimensional strings that work with DynamoDB's partition and sort key model. They partition by geohash prefix (covering a geographic area) and use the full geohash as the sort key. To find nearby drivers, the application computes the geohash of the rider's location, determines the neighboring geohash cells, and issues a small number of parallel DynamoDB queries — one per cell. Each query is a single-partition range scan, which DynamoDB serves in single-digit milliseconds.
The data model also handles the write-heavy nature of location updates. Each driver sends a location update every few seconds, which means millions of writes per minute. Lyft uses a TTL (time-to-live) attribute on each item so that stale locations are automatically deleted. When a driver goes offline, their location entry expires without requiring an explicit delete operation. This pattern — geohash partitioning with TTL-based expiration — is now a well-known DynamoDB design pattern for location-based services, but Lyft was among the first to prove it at massive scale.
Original DosiBridge notes for NoSQL Data Modeling
This section is written specifically for this page, not copied from a general system-design checklist. The useful way to study NoSQL Data Modeling is to connect the concept to a data path where correctness, access pattern, and future migrations matter more than benchmark numbers. That framing changes the design discussion from definitions to operating choices.
What makes this topic distinct
- Query Shape: For NoSQL Data Modeling, decide the query shape before choosing tools; otherwise the design looks clean but fails when nosql changes.
- Write Amplification: For NoSQL Data Modeling, decide the write amplification before choosing tools; otherwise the design looks clean but fails when data changes.
- Replication Lag: For NoSQL Data Modeling, decide the replication lag before choosing tools; otherwise the design looks clean but fails when modeling changes.
- Migration Safety: For NoSQL Data Modeling, decide the migration safety before choosing tools; otherwise the design looks clean but fails when nosql changes.
- Data Ownership: For NoSQL Data Modeling, decide the data ownership before choosing tools; otherwise the design looks clean but fails when data changes.
Production review question
If a team adopts NoSQL Data Modeling, what observable signal proves it helped instead of only making the architecture diagram more fashionable?
Practical metric to track
Track nosql-modeling health as a named dashboard or review item: combine one user-facing metric, one saturation metric, and one correctness metric. For NoSQL Data Modeling, this keeps the discussion grounded in behavior that can be measured after deployment instead of generic advice that sounds good in an interview but never gets verified.