File: chat-system/chat_system.py

Date: 2026-06-05

Time: 13:21

chat-system/chat_system.py

Purpose

This file is a single-server, in-memory chat system implementing the core concepts you'd discuss in a "Design a Chat System" interview. It owns all messaging state — connections, conversations, message routing, presence, read receipts, and group management — in one ChatServer class. It's a pedagogical implementation: no network layer, no persistence, no sharding — just the domain logic that a real system like WhatsApp or Slack would distribute across many services.

Key Components

Data Models

Message — The central data unit. Carries both content and ordering metadata:

Conversation — Container for an ordered message list plus participant set. The nextsequence counter is the source of truth for sequence number assignment. Serves both DMs and groups (distinguished by isgroup).

GroupInfo — Group metadata separated from the conversation itself. Tracks members, admins, creator_id. The membership set here is the authoritative one for permission checks; Conversation.participants mirrors it.

UserConnection — Per-user connection state with two queues: inbox (for online/away users) and offline_queue (buffered until reconnect). This models the fan-out delivery pattern where the server decides routing based on presence.

ChatServer

The monolith. Key index structures:

| Field | Type | Purpose |

|-------|------|---------|

| messages | dict[str, Message] | Global message lookup by ID — enables edit/delete/mark-read by ID |

| conversations | dict[str, Conversation] | All conversations keyed by computed or generated ID |

| readcursors | dict[tuple, int] | (userid, convid) → lastread_sequence — sequence-based, not message-ID-based |

| user_conversations | dict[str, set] | Reverse index: user → their conversation IDs |

| contacts | dict[str, set] | Bidirectional contact graph, built implicitly by messaging |

| lamport_clock | int | Single global Lamport counter incremented on every message send |

Key Methods

sendmessage — The DM path. Creates the conversation lazily on first message, assigns sequence + Lamport timestamps, delivers via deliver, auto-marks the sender's own message as read, and builds up the contact graph and conversation index as side effects.

sendgroupmessage — The group path. Validates sender membership, then fans out to all group members except the sender via _deliver.

deliver — The routing decision point. Online/away users get messages in their inbox; offline users get them in offlinequeue. This is the write-path analog of push vs. pull.

connect / disconnect — Manage presence transitions and flush the offline queue to inbox on reconnect. Both generate SYSTEM messages to the user's contacts — this is presence notification fan-out.

gethistory — Cursor-based pagination using bisect for O(log n) cursor lookup. Supports both forward and backward traversal. Returns (page, nextcursor) where next_cursor is None when exhausted.

Patterns

Conversation ID as canonical key for DMs: dmconversation_id sorts the two user IDs lexicographically and produces "dm:{u1}:{u2}". This guarantees exactly one conversation per pair regardless of who messages first — a classic deduplication technique.

Lamport clocks for causal ordering: Every sendmessage and sendgroupmessage increments a global lamportclock. This gives a total order across all conversations on this server. In a distributed version, each server would maintain its own clock and merge on receipt.

Sequence numbers for per-conversation ordering: Independent from Lamport timestamps. Sequence numbers are scoped to a conversation and used for read cursors and pagination. This separation is deliberate — you want local dense ordering for pagination but global logical ordering for consistency.

Lazy resource creation: Conversations, connections, contacts, and conversation indices are all created on first use. There's no explicit "create DM" step — send_message handles it.

Soft deletes: delete_message sets deleted=True and overwrites content with "[deleted]" but keeps the message in the list. This preserves sequence number continuity and avoids holes in pagination.

Dual-queue delivery: The inbox / offline_queue split models the real-world pattern where a chat server buffers messages for disconnected clients and flushes them on reconnect, rather than requiring the client to poll.

Dependencies

Imports: All stdlib — uuid for message/group IDs, bisect for pagination cursor lookup, dataclasses for data models, enum for status/type enums, typing for Optional.

Imported by: testchatsystem.py — the test suite. No other modules depend on this; it's a self-contained implementation.

Flow

A typical message lifecycle:

1. Sender calls send_message → conversation created if needed → Lamport clock incremented → sequence number assigned → Message constructed and appended to conversation

2. deliver called for recipient → checks recipient's UserStatus → routes to inbox (online/away) or offlinequeue (offline)

3. Recipient reconnects (connect) → offline_queue flushed to inbox → presence notification sent to contacts

4. Recipient reads messages → calls markread with a message ID → server resolves to sequence number → updates readcursors

5. Unread count queriedgetunreadcount computes lastseq - lastread — O(1)

For groups, step 2 fans out to all members except sender.

Invariants

Error Handling

Minimal and deliberate:

Topics to Explore

Beliefs