Skip to content
← All patterns

social

Notifications Feed

What’s new for this user?

A per-user feed read a page at a time, with unread as a sparse marker.

A feed is an item collection that never stops growing, so it is never read whole. This model is about the cursor: a page of notifications, a key to resume from, and an unread marker that only unread items carry.

The model

Notification

One notification, newest sorting last.

pk
USER#<userId>
sk
NOTIF#<createdOn>#<notificationId>

Attributes: kind (S), actor (S), createdAt (N), unread (S), GSI1PK (S), GSI1SK (S)

Access patterns

  • QueryRead a page of the feed

    One user's notifications, newest first, a page at a time.

  • Query · GSI1Count what is unread

    Only the unread notifications, from an index that holds nothing else.

Design notes

The cursor is a key, not an offsetwhy

A page comes back with a LastEvaluatedKey, and the next request passes it as ExclusiveStartKey. There is no page number and no OFFSET: the read resumes at an exact position in the collection, so page fifty costs what page one costs. A relational offset walks and discards everything before it, which is why deep pagination gets slower there and does not here.

Unread is absent, not falsewhy

Read notifications carry neither the unread attribute nor the index keys. That is what makes GSI1 sparse: an item with no GSI1PK is simply not in the index, so the index holds only unread notifications and the badge count is a read of something small rather than a walk of the whole feed. Marking one read means removing its index keys, and it leaves the index on its own - no delete, no sweep.

The page size is the request's businesstrade-off

Limit belongs to the read, not to the table, so the same collection serves a panel showing five and a full page showing fifty. The console here reads the collection without a Limit, so it returns every notification rather than the first page of one.

Taught in the course

notifications-feedDynamoDB workbench
Ready to run
Explore an access patternSelect to load & run

One user's notifications, newest first, a page at a time.

Request
Execute against the local Dynoxide engine
ReturnedFiltered outChanged
PK(pk)
SK(sk)
GSI1PK
GSI1SK
actor
createdAt
kind
unread
USER#u-ada
NOTIF#2026-09-07T00:00:00Z#n-8041S
—
—
samS
1788739200N
commentS
—
USER#u-ada
NOTIF#2026-09-13T00:00:00Z#n-8320S
—
—
rajS
1789257600N
assignedS
—
USER#u-ada
NOTIF#2026-09-18T00:00:00Z#n-8604S
—
—
meiS
1789689600N
mentionS
—
USER#u-ada
NOTIF#2026-09-20T00:00:00Z#n-8790S
UNREAD#u-adaS
2026-09-20T00:00:00Z#n-8790S
samS
1789862400N
commentS
yS
USER#u-ada
NOTIF#2026-09-21T00:00:00Z#n-8871S
UNREAD#u-adaS
2026-09-21T00:00:00Z#n-8871S
meiS
1789948800N
review_requestedS
yS
USER#u-ada
NOTIF#2026-09-21T19:12:00Z#n-9012S
UNREAD#u-adaS
2026-09-21T19:12:00Z#n-9012S
rajS
1790017920N
mentionS
yS
USER#u-raj
NOTIF#2026-09-15T00:00:00Z#n-8402S
—
—
meiS
1789430400N
mentionS
—
USER#u-raj
NOTIF#2026-09-21T12:00:00Z#n-8998S
UNREAD#u-rajS
2026-09-21T12:00:00Z#n-8998S
adaS
1789992000N
commentS
yS
Awaiting request
Your next query starts here.

Choose an access pattern above, or build your own request. See what comes back and what it costs.

Write transactions, streams, tags and TTL are among the operations this browser build leaves out. dynoxide's native build has them.