DB Selection
Access Patterns#
Two primary queries drive this system:
Write: A notification is dispatched to a user — insert a new notification record with status PENDING, then update it to DELIVERED or FAILED after the worker hears back from the external provider.
Read: What is the status of a notification sent to a user? Show me all notifications for a user in the last 3 months, grouped by status. This is also the analytics query — status-wise counts over a time window.
So the pattern is: write-heavy at intake, read-by-user at query time.
Why Not Relational (PostgreSQL / MySQL)?#
The first instinct is relational — structured data, familiar, ACID guarantees. But relational DBs are fundamentally read-optimised. A well-tuned PostgreSQL instance can handle around 10K–50K writes/sec before it starts struggling — WAL contention, index update overhead, and connection pool saturation all compound at scale.
We have 10M writes/sec at peak (5M notification inserts + 5M status updates from workers). That's 200x what a relational DB can comfortably sustain. No amount of vertical scaling fixes this — you'd need sharding, and once you shard a relational DB you lose most of the guarantees that made it attractive.
Relational is out for the notifications table.
Why Not MongoDB?#
MongoDB is document-oriented and more write-friendly than relational, but it still struggles at this scale. WiredTiger (MongoDB's storage engine) has similar write amplification issues at 10M/sec — particularly for updates (status changes trigger document rewrites). MongoDB's write ceiling per node sits in a similar ballpark to relational DBs for update-heavy workloads.
MongoDB is out for the notifications table.
Why Cassandra?#
Cassandra is a wide-column store designed from the ground up for write-heavy, high-throughput workloads. Its write path is optimised: every write goes to an in-memory memtable and a sequential append to a commit log — no random disk I/O, no index contention, no locking. This gives Cassandra a write throughput of 100K–200K writes/sec per node for typical payloads.
At 10M writes/sec:
300 Cassandra nodes to serve peak write throughput with fault tolerance. That's large but entirely standard for this scale — Cassandra was built for exactly this.
Cassandra's eventual consistency model is fine here. Notification status being slightly stale for a few hundred milliseconds is acceptable — we said eventual consistency for delivery status is tolerable in the NFR.
Why Not Cassandra for Preferences?#
User preferences are a different access pattern entirely:
- Low write volume — users change notification settings rarely (maybe a few thousand writes/sec across 1B users)
- Strong consistency required — if a user opts out of SMS, the next notification must not send SMS. Eventual consistency here is a bug, not a trade-off.
- Relational structure — preferences are small, structured, and naturally fit a row-per-user model
Cassandra's tunable consistency can be pushed to strong consistency (quorum reads/writes), but you pay a latency penalty and give up the write throughput advantage — which you don't need for preferences anyway.
PostgreSQL is the right call for preferences:
- ACID guarantees — opt-out writes are immediately visible to all readers
- Low write volume fits comfortably within PostgreSQL's throughput ceiling
- Simple schema — one row per user, a few columns for channel preferences and notification type settings
- Default values bootstrapped at signup, PATCH updates only changed fields
Two DBs, Two Jobs
Cassandra handles notifications — write-heavy, eventually consistent, partitioned by user. PostgreSQL handles preferences — low write, strongly consistent, ACID.
Common Trap
Candidates pick one DB for everything. The write pattern for notifications (10M/sec, eventual consistency OK) and preferences (low write, strong consistency required) are fundamentally different problems — they need different tools.
Summary#
| Notifications | Preferences | |
|---|---|---|
| DB | Cassandra | PostgreSQL |
| Write volume | 10M/sec | Low |
| Consistency | Eventual | Strong |
| Reason | Write-optimised, horizontally scalable | ACID, strong consistency, low volume |
| Nodes | 300 (100 shards × 3 replicas) | Standard replicated PostgreSQL |