Why Single-Table Design is the Ultimate Superpower for Your Serverless Blog

If you've ever built a blog using traditional SQL databases, you know the drill: a users table, a posts table, a comments table... and then a bunch of messy JOIN operations that slow down your page load times as your traffic scales.
But what if I told you there's a better, cheaper, and blazingly fast way? Enter DynamoDB Single-Table Design.
Instead of spreading data across multiple tables, we stuff everything—users, settings, posts, and comments—into a single NoSQL table. Thanks to clever Partition Keys (PK), Sort Keys (SK), and Global Secondary Indexes (GSIs), AWS can fetch data in a single, ultra-cheap query. Let's dive into how we built this for our serverless blog platform!
🚀 The Terraform Magic
I use Infrastructure as Code (IaC) to guarantee every blog environment is identical and reproducible. Here is heavily simplified snippet of how we spin up our glorious Single-Table using Terraform, including our Global Secondary Indexes for advanced lookups:
# --- DynamoDB Main Table ---
resource "aws_dynamodb_table" "blog_table" {
name = "${var.project_name}-table"
billing_mode = "PAY_PER_REQUEST" # Ultra-cheap: Pay only for actual usage (On-Demand)
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
# Partition Key & Sort Key
hash_key = "PK"
range_key = "SK"
attribute { name = "PK" type = "S" }
attribute { name = "SK" type = "S" }
attribute { name = "slug" type = "S" }
attribute { name = "GSI1PK" type = "S" }
attribute { name = "GSI1SK" type = "S" }
global_secondary_index {
name = "SlugIndex"
key_schema {
attribute_name = "slug"
key_type = "HASH"
}
projection_type = "ALL"
}
global_secondary_index {
name = "TypeIndex"
key_schema {
attribute_name = "GSI1PK"
key_type = "HASH"
}
key_schema {
attribute_name = "GSI1SK"
key_type = "RANGE"
}
projection_type = "ALL"
}
}
Notice the
PAY_PER_REQUESTbilling mode? That means if you get zero visitors today, you pay $0.00. No idling servers draining your wallet!
🧠 The Python Lambda CRUD Connection
Having a table is great, but how do we interact with it? We don't talk to the database directly from the frontend. I use Python 3.12 AWS Lambda functions to handle our CRUD (Create, Read, Update, Delete) logic.
For example, my posts_handler Lambda function uses a ULID (Universally Unique Lexicographically Sortable Identifier) layer to push items to DynamoDB that look like this:
PK:USER#<unique_author_id>SK:POST#<ulid>slug:my-awesome-vacationGSI1PK:POSTGSI1SK:2026-04-13T09:46:00Z
Because Lambdas are stateless and scale instantly, if your blog post goes viral, AWS simply spins up more Lambdas concurrently to handle the bursts of traffic. Thanks to our SlugIndex (GSI), I can incredibly fast-fetch any post just by its URL-friendly slug.
💬 The "Item Collection" Pattern for Comments
You might be wondering: if I don't have a comments table, where do the comments go?
They go in the exact same table! This is where the magic of the Item Collection pattern comes into play. Because we pass the aws_dynamodb_table.blog_table.name to our comments_handler Lambda just as we do for the posts_handler, we can group a post and its comments together using the Partition Key (PK).
For a given post, the items in DynamoDB look like this:
Post Item:
PK=POST#<ulid>|SK=METADATA#<ulid>|title= "Hello World"Comment 1:
PK=POST#<ulid>|SK=COMMENT#<ulid>|author= "Alice"Comment 2:
PK=POST#<ulid>|SK=COMMENT#<ulid>|author= "Bob"
The Advantage? Zero SQL JOINs. When a user opens a blog post, our API doesn't need to query a
poststable and then go query acommentstable. We execute a single DynamoDBQueryforPK = POST#<ulid>. AWS automatically returns the post metadata and all of its comments simultaneously. This fundamentally eliminates the1+Nquery problem that cripples relational databases under heavy load. This is the essence of why Single-Table Design is so powerful!
🔒 Who Gets In? (Cognito & API Gateway)
You might be wondering: "If the API can read and write posts, how do we stop random internet trolls from editing my blog?"
This is where the beautiful marriage of API Gateway and Amazon Cognito shines. We place an HTTP API Gateway directly in front of our Lambdas with explicit integration routes:
✅ Public Routes (
GET /posts): We leave the read-only endpoints completely open. Anyone in the world can fetch a post's JSON list without needing to log in.🛑 Private Routes (
POST /posts,PATCH /posts/{id},DELETE /posts/{id}): For admin actions, we attach a JWT Authorizer linked directly to our Cognito User Pool. Before the application even bothers waking up your Lambda function, API Gateway natively checks if the user has a valid JWT token. If they aren't logged in, they get slapped with a401 Unauthorizedinstantly.
Wrapping Up
By combining DynamoDB Single-Table Design, Terraform, Python Lambdas, Event-Driven Streams, and Cognito, you get a blog architecture that scales infinitely, costs fractions of a penny, and natively integrates with GitHub Actions for instant publishing.
Time to stop managing SQL servers and start creating!
Support the Journey.
I create content and share knowledge from the serverless frontier. If you found this helpful, consider buying me a coffee!
Buy me a coffeeComments (0)
You must be logged in to comment.
Login to Comment