Skip to content

Pending Deliveries Table

A DynamoDB table that tracks exactly which conversations have undelivered messages for each offline user

Durable by default, queryable by receiver_id, stores only what's needed — one row per pending conversation.


The table design

Table: pending_deliveries

PK: receiver_id          → bob
SK: conversation_id      → conv_abc123
Attribute: first_undelivered_seq → 42

One row per (user, conversation) pair that has undelivered messages. That's it.


Why this design works

Durable: DynamoDB replicates across 3 availability zones. No AOF sync windows, no restart concerns. A write to pending_deliveries is safe the moment it returns success.

Queryable by receiver: PK=bob returns all conversations with pending messages for Bob in a single range scan. No scanning other users' data.

Lightweight: One row per conversation, not one row per message. Bob could have 50 undelivered messages from Alice and it's still just one row:

{ receiver_id: bob, conversation_id: conv_abc123, first_undelivered_seq: 42 }

On reconnect, SK > 41 on the messages table returns all 50. The pending_deliveries table never grows with message volume — only with conversation count.


Write flow — when a message arrives for an offline user

Alice sends seq=42, Bob is offline:
  → Check pending_deliveries: GET {bob, conv_abc123}
  → No entry exists
  → Write: { receiver_id: bob, conversation_id: conv_abc123, first_undelivered_seq: 42 }

Alice sends seq=43, Bob still offline:
  → Check pending_deliveries: GET {bob, conv_abc123}
  → Entry already exists with first_undelivered_seq=42
  → Do nothing — seq=43 will be fetched automatically when server queries SK > 41

The entry is written only once per conversation per offline session. Subsequent messages to the same conversation don't update it — the DynamoDB range query SK > first_undelivered_seq naturally catches everything.


Why first_undelivered_seq and not last_undelivered_seq

The server stores the first message Bob missed, not the last. On reconnect:

Query: PK=conv_abc123, SK >= first_undelivered_seq

This returns everything from that point forward — including messages that arrived after the entry was written. No need to update the entry every time a new message arrives. The DynamoDB query dynamically catches up to the current state of the conversation.


Cleanup after delivery

Once Bob reconnects and all pending messages are delivered:

→ Delete entry from pending_deliveries: {bob, conv_abc123}
→ Update last_delivered_seq tracker to the latest seq delivered

The table stays lean — entries only exist for users who are currently offline with pending messages.