| name | nosql-patterns |
| description | NoSQL database patterns: MongoDB document design (embedding vs. referencing), DynamoDB single-table design with access patterns, Redis as primary store, and when to use each NoSQL database vs. Postgres. |
NoSQL Patterns Skill
When to Activate
- Evaluating whether to use MongoDB, DynamoDB, or Redis over Postgres
- Designing a MongoDB schema (embedding vs. referencing decisions)
- Designing DynamoDB tables (single-table design, access patterns)
- Building high-throughput key/value or session storage
- Handling unstructured or highly variable document shapes
- Defining all DynamoDB access patterns upfront before committing to a key schema and GSI layout
- Deciding whether to embed or reference child data in MongoDB to avoid unbounded document growth or N+1 lookup patterns
When to Choose NoSQL vs. Postgres
| Situation | Choose | Reason |
|---|
| Relational data, joins, ACID transactions | Postgres | Best default |
| Documents with highly variable schema | MongoDB | Flexible schema |
| Serverless, auto-scaling, single-digit ms latency | DynamoDB | AWS-native, infinite scale |
| Ephemeral data, sessions, rate limiting, pub/sub | Redis | In-memory, TTL built-in |
| Analytical queries, Parquet/CSV files, local OLAP | DuckDB | Embedded, zero infra, columnar |
| Time-series (metrics, events) | TimescaleDB | Optimized for append + range queries |
| Graph relationships | Neo4j or Postgres + pgvector | Purpose-built |
Default: Postgres. Only switch to NoSQL when there's a specific, concrete reason.
MongoDB: Document Design
Embedding vs. Referencing
Embed when:
- Data is always accessed together (one query is better than two)
- Child data doesn't grow unboundedly
- Child doesn't need to be accessed independently
Reference when:
- Many-to-many relationship
- Child data is large and not always needed
- Child is shared across multiple parents
- Child grows unboundedly (e.g., comments on a post)
{
_id: ObjectId("..."),
title: "My Post",
comments: [
{ user: "alice", text: "Great post!" },
]
}
{
_id: ObjectId("post123"),
title: "My Post",
authorId: ObjectId("user456"),
commentCount: 42,
createdAt: ISODate("2024-01-15")
}
{
_id: ObjectId("..."),
postId: ObjectId("post123"),
userId: ObjectId("user456"),
text: "Great post!",
createdAt: ISODate("2024-01-15")
}
{
_id: ObjectId("order123"),
customerId: (),
: {
: ,
: ,
: ,
:
},
: [
{ : (), : , : , : },
{ : (), : , : , : }
],
: ,
:
}
Indexes
db.orders.createIndex({ customerId: 1, createdAt: -1 });
db.comments.createIndex({ postId: 1, createdAt: -1 });
db.products.createIndex({ name: "text", description: "text" });
db.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
db.users.createIndex({ email: 1 }, { unique: true });
db.orders.createIndex(
{ assignedTo: 1, createdAt: -1 },
{ partialFilterExpression: { status: 'open' } }
);
TypeScript with Mongoose
import { Schema, model, Document, Types } from 'mongoose';
interface IOrder extends Document {
customerId: Types.ObjectId;
items: Array<{ productId: Types.ObjectId; name: string; qty: number; price: number }>;
total: number;
status: 'pending' | 'shipped' | 'delivered' | 'cancelled';
createdAt: Date;
}
const orderSchema = new Schema<IOrder>(
{
customerId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true },
items: [{
productId: { type: Schema.Types.ObjectId, ref: , : },
: { : , : },
: { : , : , : },
: { : , : , : },
}],
: { : , : },
: { : , : [, , , ], : },
},
{ : }
);
= model<>(, orderSchema);
DynamoDB: Single-Table Design
DynamoDB requires you to define all access patterns upfront. Start there.
Step 1: Define Access Patterns
Entity: User, Order, Product
Access patterns:
1. Get user by ID
2. Get order by ID
3. Get all orders for a user (newest first)
4. Get all pending orders (across all users)
5. Get product by ID
Step 2: Design Key Schema
Table: AppTable
PK (Partition Key): string
SK (Sort Key): string
Entities:
USER | PK: USER#userId | SK: USER#userId
ORDER | PK: USER#userId | SK: ORDER#orderId
PRODUCT | PK: PRODUCT#id | SK: PRODUCT#id
GSI1 (for access pattern 4 — query by status):
GSI1PK: ORDER_STATUS#status | GSI1SK: ORDER#createdAt
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, QueryCommand, PutCommand } from '@aws-sdk/lib-dynamodb';
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.DYNAMODB_TABLE!;
async function getUser(userId: string) {
const result = await docClient.send(new GetCommand({
TableName: TABLE,
Key: { PK: `USER#${userId}`, SK: `USER#${userId}` },
}));
return result.Item;
}
async function getUserOrders(userId: string) {
result = docClient.( ({
: ,
: ,
: {
: ,
: ,
},
: ,
}));
result.;
}
() {
docClient.( ({
: ,
: {
: ,
: ,
: ,
: ,
...order,
},
}));
}
Redis as Primary Store
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
async function createSession(sessionId: string, userId: string, ttlSeconds = 86400) {
await redis.setEx(
`session:${sessionId}`,
ttlSeconds,
JSON.stringify({ userId, createdAt: new Date().toISOString() })
);
}
async function getSession(sessionId: string) {
const key = `session:${sessionId}`;
const data = await redis.get(key);
if (!data) return null;
await redis.expire(key, 86400);
return JSON.parse(data);
}
async (): <> {
now = .();
windowStart = now - windowSeconds * ;
rateLimitKey = ;
redis.(rateLimitKey, , windowStart);
redis.(rateLimitKey, { : now, : });
count = redis.(rateLimitKey);
redis.(rateLimitKey, windowSeconds);
count > limit;
}
(): <> {
redis.(, );
}
Checklist
MongoDB:
DynamoDB:
Redis: