← System Design
AI Systems

Design ChatGPT (LLM Chat Service)

Watch: "Design ChatGPT - System Design Mock Interview (with eBay EM)" — Exponent
In simple words

A very knowledgeable friend on a phone call who speaks word by word as they think. Every time you ask something, you quickly remind them of the conversation so far (they don't remember on their own), and the longer the chat, the longer that reminder gets.

Problem Statement

Design a chat assistant like ChatGPT or Claude: users hold multi-turn conversations with a large language model, see the answer stream in token by token, and come back later to their conversation history. The unusual parts compared with a normal web app are that each request is expensive GPU work lasting seconds, the response is a stream, and the "context" sent to the model grows with every turn.

Functional Requirements

Non-Functional Requirements

Scale assumptions
  50M DAU x 10 messages/day      = 500M messages/day  ~ 6K/s avg, ~15K/s peak
  avg answer 400 tokens, prompt (with history) ~2K tokens
  output tokens: 6K/s x 400      = 2.4M tokens/s generated on average
  if one GPU replica serves ~2-5K tokens/s with batching -> ~500-1,000+ replicas
  storage: 500M x ~4 KB (both sides) ~ 2 TB/day of conversation text

Core Entities

API Design

POST /v1/conversations                         -> { conversation_id }
POST /v1/conversations/{id}/messages
  { "content": "Explain CAP", "model": "large" }
  -> 200 text/event-stream
     data: {"type":"token","text":"CAP"}
     data: {"type":"token","text":" says"} ...
     data: {"type":"done","message_id":"m_9","usage":{"in":2048,"out":412}}
POST /v1/generations/{id}/cancel               -> 204
GET  /v1/conversations?cursor=...              GET /v1/conversations/{id}/messages

High-Level Design (HLD)

Chat request: from message to streamed answerBrowser / appAPI gateway(auth, ratelimit)Chat service(builds prompt)ConversationstoreInferencegatewayModel servers(GPU, batching)Usage eventssendmessageloadhistory· savepromptSSE token stream
The chat service rebuilds the prompt from history each turn and streams tokens back as the model produces them.

Low-Level Design (LLD)

Data modelconversationsconversation_id PKuser_id, title, modelsummary, updated_atmessagesconversation_id PKmessage_id PKparent_id, rolecontent, tokensusageuser_id PKday PKtokens_in, tokens_out1 → manymetered
parent_id lets a regenerated answer branch from the same user message; usage feeds quotas and billing.

Building the prompt: the context window

budget = model_context (e.g. 128K tokens) - reserved_for_answer (e.g. 4K)
prompt = [system prompt]
       + [summary of older turns]            # when history exceeds the budget
       + [most recent turns, newest last]    # as many as fit
       + [new user message]

Summaries are produced asynchronously and stored on the conversation, so
long chats don't resend (or re-pay for) their whole history every turn.

Providers cache the key/value attention state for a repeated prefix ("prompt caching"), so keeping the system prompt and older history byte-identical between turns makes each new turn cheaper and faster.

Streaming end to end

Data model

conversations(conversation_id PK, user_id, title, model, summary, updated_at)
   -- index (user_id, updated_at DESC) for the sidebar list
messages(conversation_id, message_id, parent_id, role, content, tokens, created_at,
         PRIMARY KEY (conversation_id, message_id))
   -- partitioned by conversation_id: one conversation = one partition, read in order
usage(user_id, day, tokens_in, tokens_out)   -- quotas and billing

Capacity and overload

When GPUs are saturated, the gateway queues requests with a short deadline, sheds free-tier traffic first, or falls back to a smaller model; per-user token-bucket limits stop one user from monopolising capacity.

Follow-up Questions

Why SSE and not WebSockets?
Each answer is a one-way stream in response to one request. SSE is plain HTTP, works through proxies and load balancers, reconnects automatically, and needs no bidirectional protocol.
How do you keep costs down?
Continuous batching on GPUs, prompt/prefix caching, trimming and summarising history, routing simple requests to smaller models, and caching answers to identical popular prompts where appropriate.
A GPU node dies mid-answer. What does the user see?
The stream errors; the client offers "regenerate", which re-sends the same prompt to a healthy replica. Generation state isn't worth replicating: it's cheaper to redo.
How would you add retrieval over the user's documents?
Before calling the model, retrieve relevant chunks from a vector index scoped to the user and add them to the prompt: that is RAG, covered in the RAG chatbot design.

Related