System Design: Design Twitter DM — Direct Messaging, End-to-End Encryption, Read Receipts, Media, Group Conversations

Twitter Direct Messages (DMs) serve hundreds of millions of users with private 1-on-1 and group conversations. Designing a DM system within an existing social platform tests your understanding of messaging infrastructure layered on top of a social graph, encryption for private conversations, and the unique challenges of DMs versus a standalone chat app (WhatsApp). This guide focuses on the DM-specific architecture.

Data Model and Conversations

Core entities: (1) Conversation: conversation_id, type (1-on-1 or group), participant_ids, created_at, last_message_at, name (for groups), avatar (for groups). For 1-on-1: the conversation_id can be derived from the sorted pair of user_ids (deterministic — no need to create a conversation record before the first message). (2) Message: message_id (Snowflake time-sorted), conversation_id, sender_id, content (encrypted text), media_urls, reply_to_message_id, reactions, created_at, deleted_at. (3) Participant state: conversation_id, user_id, last_read_message_id (for read receipts and unread count), muted (boolean), notification_preference. Inbox: each user has a DM inbox — a list of conversations ordered by last_message_at descending. Loading the inbox: SELECT conversations WHERE user_id IN participants ORDER BY last_message_at DESC LIMIT 20. Pre-compute and cache the inbox in Redis as a sorted set: ZADD inbox:{user_id} {last_message_timestamp} {conversation_id}. Update the score when a new message arrives in any conversation the user is part of. Unread count: for each conversation, unread_count = count of messages with message_id > last_read_message_id. Pre-compute in Redis: INCR unread:{user_id}:{conversation_id} on new message. Reset to 0 when the user reads.

Message Delivery

When User A sends a DM to User B: (1) The API validates: A is allowed to DM B (B DM settings: open, followers only, verified only). B has not blocked A. Content passes policy checks. (2) The message is stored in the messages database (Cassandra, partitioned by conversation_id, clustered by message_id for chronological order). (3) The conversation last_message_at is updated. A inbox sort score is updated. B inbox sort score is updated. B unread count is incremented. (4) Real-time delivery: if B is online (has an active WebSocket/gateway connection), push the message via the existing Twitter gateway connection. DM events share the same WebSocket as tweet events, notifications, and presence. (5) If B is offline: send a push notification (APNs/FCM). The notification shows sender name and a preview of the message (unless B has notification previews disabled for privacy). (6) Delivery receipt: when B device receives the message, it sends an acknowledgment. The sender sees a delivery indicator (single check -> double check, similar to WhatsApp). (7) Read receipt: when B opens the conversation, the client sends a “read up to message_id X” event. A sees the read indicator (blue check). Read receipts can be disabled per-user in privacy settings.

Group DMs

Twitter supports group DMs with up to 50 participants. Group-specific features: (1) Any participant can add people (unless the conversation is locked by the creator). (2) Name and avatar for the group. (3) Leave group — the user stops receiving messages. They can rejoin if re-added. (4) Admin controls (limited): the creator can remove participants. Message fanout for groups: when a message is sent to a 20-person group, 19 inbox updates + 19 unread increments + 19 real-time pushes (for online members) + push notifications for offline members. This is small-scale fanout (not the Twitter feed celebrity problem with millions of followers). For 50-person groups: 49 operations per message. Group conversations with high message volume (many active participants): the inbox update and unread increment happen for every message for every participant. With 10 active chatters sending 1 message/minute: 490 inbox updates per minute for this group alone. Redis handles this easily, but for users in many active groups, the inbox update frequency can be high. Optimization: batch inbox updates — instead of updating the inbox on every message, update every 5 seconds (or on the next client poll). The slight delay is imperceptible.

Encryption

Twitter introduced encrypted DMs (limited, opt-in). Architecture: Signal Protocol-based (same as WhatsApp): each user generates a public-private key pair. Public keys are registered on the server. To send an encrypted DM: the sender fetches the recipient public key, derives a shared secret via X3DH (Extended Triple Diffie-Hellman), and encrypts the message with AES-256-GCM using the shared secret. The server stores only the ciphertext — it cannot read the message. The recipient decrypts using their private key. Double Ratchet: after the initial key exchange, each message uses a new symmetric key derived from the previous (the “ratchet” advances). This provides forward secrecy: compromising the current key does not expose past messages. Limitations of encrypted DMs: (1) The server cannot index or search encrypted messages — search is client-side only. (2) Content moderation cannot scan encrypted messages — policy violations in encrypted DMs can only be reported by participants. (3) Multi-device: the encryption keys must be synced across the user devices. Each device has its own key pair. Sending to a user with 3 devices requires encrypting the message 3 times (once per device key). (4) Group encryption: each message is encrypted separately for each participant (N encryptions for N participants). For a 50-person group: 49 encryptions per message.

Media and Reactions

Media in DMs: images, GIFs, videos, and voice messages. Upload flow: presigned S3 URL (same pattern as tweet media). The media is processed (resize, thumbnail, virus scan) and the URL is attached to the message. For encrypted DMs: the media file is encrypted client-side before upload. The S3 stores only ciphertext. The encryption key is included in the encrypted message payload. The recipient downloads and decrypts the media locally. Voice messages: the client records audio, compresses with Opus codec, uploads as a media attachment. Duration limit: 140 seconds (matching the original tweet character limit theme). The DM shows a waveform visualization and play button. Reactions: users can react to DM messages with emojis. Stored per-message: reactions list of (user_id, emoji). When a reaction is added: push a real-time update to all conversation participants. The message UI shows reaction emojis with counts. Message editing: users can edit sent messages within a time window (15 minutes). The edit updates the message content in the database. A “edited” label is shown. The edit event is pushed to all participants. Message deletion: “delete for me” removes from the user local view. “Delete for everyone” (within 15 minutes) marks the message as deleted and pushes a deletion event to all participants. The message content is replaced with “This message was deleted.”

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do DM read receipts and delivery indicators work?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Two-stage delivery tracking: (1) Delivery receipt — when the recipient device downloads the message, it sends an acknowledgment to the server. The sender sees a delivery indicator (double checkmark). This confirms the message reached the device, not that the user read it. (2) Read receipt — when the recipient opens the conversation, the client sends read up to message_id X to the server. The sender sees a read indicator (blue checkmark). Read receipts are privacy-sensitive: users can disable them in settings. When disabled, the sender never sees the read indicator, and the user does not generate read events for others either (symmetric — if you disable reading others receipts, they cannot see yours). Implementation: read state is stored as last_read_message_id per user per conversation. Unread count = messages with id > last_read_message_id. Cached in Redis for fast access.”}},{“@type”:”Question”,”name”:”How does end-to-end encryption work for DMs?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Signal Protocol: each user generates a public-private key pair. To send an encrypted DM: fetch recipient public key, derive shared secret via X3DH key exchange, encrypt with AES-256-GCM. The server stores only ciphertext. Double Ratchet advances the key after each message for forward secrecy (compromising the current key does not expose past messages). Limitations: (1) Server cannot search encrypted messages — search is client-side only. (2) Content moderation cannot scan encrypted DMs — violations are only detected through user reports. (3) Multi-device: each device has its own key pair. Sending to a user with 3 devices requires 3 encryptions. (4) Group DMs: each message encrypted N times (once per participant). For a 50-person group: 49 encryptions. (5) Media in encrypted DMs: files are encrypted client-side before upload to S3. The decryption key is sent inside the encrypted message.”}}]}
Scroll to Top