I made a mistake that most developers make when starting a new project.
I opened the AWS console, looked at the database options, and thought: "I just need to pick one."
Two weeks later, after building STRATUM — a real-time neighborhood intelligence platform — I understand why that instinct is wrong. Not slightly wrong. Fundamentally wrong for a certain class of application.
This is the story of why I ended up using both DynamoDB and Aurora PostgreSQL, what almost made me collapse them into one, and what the experience taught me about database architecture that I won't forget.
What STRATUM does
STRATUM lets residents anonymously drop geo-tagged signals about their neighborhood in 15 seconds — a flooded road, a new coffee shop, an unsafe corner at night, a construction site that starts at 6am. The app clusters thousands of these signals into live "layers" on a map: Safety, Vibe, Infrastructure, Opportunity.
Three types of users interact with it:
Residents report and explore for free
Real estate agents pay $49/month for neighborhood trend APIs
City governments pay $299/month for infrastructure dashboards
Live app: https://stratum-rho-green.vercel.app
Here's what the map looks like with real data across San Francisco:
And here's the neighborhood score card that appears when you click any cluster:
The naive version: just use one database
My first instinct was DynamoDB for everything. The scale story is obvious — DynamoDB handles millions of writes per second, it's fully managed, it never goes down. For a platform where anyone in the world can submit a report at any moment, that write scalability is genuinely important.
The problem showed up when I tried to answer questions like:
"What is the average safety score for all neighborhoods in San Francisco over the last 7 days?"
"Which neighborhoods have seen a 30% increase in infrastructure reports this month?"
"Show me all API keys, their usage this month, and whether their subscription is still active"
DynamoDB is not built for these questions. It's built for "give me this specific item by its key." Analytical queries that scan across many items, join data from multiple entities, and aggregate over time ranges — these are expensive and awkward in DynamoDB. You can technically do them, but you're working against the grain of the database every single time.
So I tried the other direction: Aurora PostgreSQL for everything. Rich relational queries, SQL, the full power of a real database engine.
The problem showed up immediately. Every anonymous resident report is a write. In a city with millions of residents, those writes are constant, high-volume, and completely unpredictable in timing. Aurora PostgreSQL scales, but it scales differently — it needs connection pooling, it has cold start latency on serverless, and the cost model for millions of small writes is painful compared to DynamoDB.
More critically: a resident submitting a report should feel instant. Sub-100ms. Serverless Aurora adds latency that DynamoDB simply doesn't have.
The insight: these are two genuinely different problems
Here's the moment everything clicked.
The report submission flow and the analytics flow have completely different shapes:
WRITE PATH (report submission):
Extremely high volume at scale
Simple structure: one report, known fields, no joins needed
Needs to be fast everywhere in the world simultaneously
Data is naturally partitioned by geography
Old reports can expire — nobody needs 5-year-old noise complaints
READ PATH (analytics and intelligence):
Lower volume but complex queries
Requires aggregation, trends, comparisons across many records
Needs relational integrity — users linked to subscriptions linked to API keys linked to usage logs
Data needs to persist and compound over time
These aren't two versions of the same problem. They're genuinely different problems that happen to live in the same application.
This is a pattern called CQRS — Command Query Responsibility Segregation. The basic idea: separate the write model from the read model. Don't force one database to be good at both things. Let each database do what it was actually built to do.
The actual implementation
Here's how data flows through STRATUM:
POST /api/reports
│
▼
AWS DynamoDB — stratum-reports table
PK: geohash (6-character, ~1.2km² cell)
SK: timestamp_id (ISO8601#uuid)
GSI: city_id-time (for city-level queries)
TTL: 90 days automatic expiry
The partition key choice is the most important decision in the whole architecture. I'm using geohash — a system that encodes latitude and longitude into a short string where nearby locations share a common prefix. A 6-character geohash represents roughly a 1.2km² cell, which maps naturally to what people think of as a neighborhood block.
This means DynamoDB queries for "all reports in this area" hit a single partition. No expensive table scans. No scatter-gather across the whole dataset. The spatial query is essentially free at scale.
Here's what the DynamoDB table looks like in production with real seeded data:
The scoring engine reads from DynamoDB, calculates neighborhood health scores using recency-weighted severity, and writes results to Aurora:
GET /api/neighborhood/[geohash]
│
├──► DynamoDB: fetch recent reports for this geohash
│
├──► Scoring engine:
│ safetyScore = 100 - Σ(severity × recencyWeight × 3)
│ vibeScore = 50 + Σ(severity × recencyWeight × 2)
│ recencyWeight: 1.0 (today) → 0.7 (week) → 0.4 (2 weeks) → 0.2 (month)
│
└──► Aurora PostgreSQL: persist neighborhood_scores row
Aurora handles everything relational:
sqlCREATE TABLE neighborhood_scores (
geohash_4 VARCHAR(4),
safety_score DECIMAL(4,1),
vibe_score DECIMAL(4,1),
infrastructure_score DECIMAL(4,1),
opportunity_score DECIMAL(4,1),
overall_score DECIMAL(4,1),
report_count_7d INTEGER,
calculated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE api_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
plan_type VARCHAR(20),
api_key VARCHAR(64) UNIQUE,
calls_this_month INTEGER DEFAULT 0,
monthly_calls_limit INTEGER DEFAULT 10000,
stripe_subscription_id VARCHAR(100)
);
Here's the Aurora cluster running in production:
What each database handles in production
DynamoDB — stratum-reports:
Every anonymous report write (millisecond latency globally)
Map cluster reads (geohash-partitioned, zero cross-partition scans)
Automatic TTL expiry on old reports after 90 days
Zero ops: no connections to manage, no schema to migrate, no patching
Aurora PostgreSQL — stratum-db:
Neighborhood scores (persisted, queryable, comparable over time)
User accounts and authentication state
API subscription management and rate limiting
Usage logging for billing
Analytics exports for B2B customers
Complex trend queries: safety score change week-over-week by city
The mistake I almost made
Halfway through building this, I had a moment of doubt. The dual-database setup felt like complexity I'd introduced unnecessarily. Two sets of credentials. Two connection clients. Two mental models to hold at once.
I almost collapsed everything into Aurora.
What stopped me was actually benchmarking the write path. Report submission endpoint with DynamoDB: consistently under 80ms end-to-end from Vercel edge to DynamoDB and back. With Aurora Serverless on a cold start: over 800ms.
For a "tap to report" mobile interaction, that difference is everything. 80ms feels instant. 800ms feels broken.
The complexity of two databases is real. But so is the performance cost of using the wrong one.
When you should do this
This architecture makes sense when your application has a clear write-heavy operational flow AND a separate analytical or relational layer.
Use DynamoDB when:
You have high-volume writes with simple, known structure
Data is naturally partitioned by a key (user ID, location, device ID)
Latency requirements are strict globally
Data has a natural expiry (sessions, events, signals, logs)
Use Aurora PostgreSQL when:
You need complex queries across many records
You have relational data that needs integrity constraints
You're building billing, subscriptions, or any financial logic
You need aggregations, trends, and data exports
If your application has both — and many real applications do — don't choose. Use both. The operational cost is lower than you think, and the performance difference is not.
The full architecture:
The key insight: CQRS-style split. DynamoDB absorbs anonymous writes. Aurora serves durable analytics. Vercel connects them through serverless API routes. Each layer does exactly one job.
Try it live
STRATUM is live at https://stratum-rho-green.vercel.app with real data across three cities — San Francisco, Brooklyn, and Austin. The DynamoDB table holds 2,300+ real reports. Aurora holds the scoring and subscription layer.
Open the map, click any cluster, and the neighborhood score card pulls live data from both databases in a single request.
Demo login: demo@stratum.app / stratum2026
I'm publishing this post as my official content submission for the H0: Hack the Zero Stack with Vercel v0 and AWS Databases hackathon.
I created this project for the H0: Hack the Zero Stack with Vercel v0 and AWS Databases hackathon.
H0Hackathon
Stack: Next.js 14 · Vercel · AWS DynamoDB · AWS Aurora PostgreSQL 17 · MapLibre GL JS · NextAuth · Stripe





Top comments (0)