Choosing a Store for a Notifications Service
You are designing a notifications service: 200 million users, each user accumulates up to a few thousand notifications (read/unread), the dominant query is "give me this user's latest 50 notifications", writes arrive from many producers (likes, comments, follows) at ~30,000/s peak, and there is no requirement for joins or ad-hoc analytical queries on this data.
- Which storage model best fits this workload, and why?
- Design the partition key and sort key (or equivalent) for the dominant query.
- What is the one requirement that, if added later, would force you to reconsider the choice?
1. Storage model
A wide-column store (Cassandra/ScyllaDB) or a key-value/document store
with a partition+sort model (DynamoDB) fits well. The access pattern is
entirely "per user, most recent N, ordered" — no joins, no ad-hoc
queries, high write volume from many independent producers. That is
exactly the shape wide-column and DynamoDB are built for: LSM-based
write paths absorb 30k/s comfortably, and the query is a single
partition range scan. A relational database could serve this too (a
notifications table indexed on (user_id, created_at DESC)), but at
this write rate and with no relational requirement, it would need
sharding by user_id to keep up — which is exactly what the wide-column
store gives you for free.
2. Key design
Partition key: user_id. Sort key: a time-ordered value, e.g.
created_at or a Snowflake-style ID that sorts by time
(notification_id). The dominant query becomes: read the partition for
user_id, scan the sort key in descending order, limit 50 — a single
efficient range read with no fan-out. Mark read/unread as an attribute
on the item (or a separate unread_count counter item) rather than a
separate table, since it is always read alongside the notification.
3. What would force reconsideration
A requirement like "notify a user's manager whenever 3+ unread HR
notifications accumulate across the org" or any cross-user, ad-hoc,
relational query (joins across users, arbitrary filtering not keyed by
user_id) would not be servable by this model without scatter-gather or
a secondary system. At that point you would keep the wide-column store
for the hot per-user read path and add a relational or search store fed
by CDC for the cross-cutting query — polyglot persistence, justified by
a specific new access pattern rather than adopted upfront.
Share this question