1. Clarify the requirements

Functional requirements

Pick a small core. A safe set:

Five flows is enough to force every interesting tradeoff: persistent connections vs. polling, push delivery vs. pull, ephemeral state vs. durable history, fanout to N recipients, and decoupling bytes from messages. Explicitly defer: end-to-end encryption, multi-device sync, voice/video calls, status (stories), payments, search, communities/broadcast at huge scale, message reactions, replies/quotes.

Non-functional requirements

These shape the architecture more than the features do.

Core entities

Before sizing or APIs, name the objects you’re modeling. Fields and storage come later:

Presence and MessageStatus aren’t stored the same way as messages — one is in-memory with a TTL, the other is a per-(message, recipient) row that mutates a few times. Calling them out now is what makes the presence chapter (6) and receipts chapter (5) meaningful later.

Core entities at a glance

flowchart TD
User(["User"])
Conv(["Conversation"])
Msg(["Message"])
Status(["MessageStatus
(per recipient)"])
Group(["Group"])
Pres(["Presence
(ephemeral)"])

User -->|"sends"| Msg
Msg -->|"belongs to"| Conv
Group -->|"is a"| Conv
Msg -->|"has"| Status
User -->|"member of"| Group
User -->|"has"| Pres