How would you maintain conversation state in a chat app like ChatGPT?

HighIntermediateJavascript
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

This probes your ability to model a conversation, manage streaming messages, and handle memory limits or truncation without breaking UX.

Answer

Definition (above the fold)

To maintain conversation state in a chat app like ChatGPT, model chat history as an append-only timeline with stable IDs, explicit status transitions, and a separate streaming draft buffer. The key is deterministic state transitions: user submit, optimistic render, stream append, finalize, retry/fail. If these transitions are implicit, you get duplicate messages, stale drafts, and broken ordering under packet delay. Interviewers want a state model, not only UI screenshots. Strong answers also define persistence boundaries and recovery behavior for offline/reconnect cases, not only happy-path streaming.

Core mental model

Use a finite-state message lifecycle: pending -> streaming -> done | error. Keep canonical message entities separate from UI-only controls (composer text, active request id, autoscroll lock) so retries and reconnects are predictable.

State slice

Must-have fields

Why

messagesById

id, role, content, status, createdAt

Stable rendering and reconciliation

orderedIds

array of message IDs

Deterministic ordering even with retries

activeStream

requestId, assistantMessageId, cursor

Safe chunk routing and cancellation

historyPolicy

tokenBudget, summaryCheckpointId

Bounded memory and request size

Separate canonical message state from UI helper state.

Runnable example #1: reducer transitions

JAVASCRIPT
function reducer(state, action) {
  switch (action.type) {
    case 'user/submitted': {
      const userId = action.userId;
      const assistantId = action.assistantId;
      return {
        ...state,
        orderedIds: [...state.orderedIds, userId, assistantId],
        messagesById: {
          ...state.messagesById,
          [userId]: { id: userId, role: 'user', content: action.text, status: 'done' },
          [assistantId]: { id: assistantId, role: 'assistant', content: '', status: 'streaming' }
        },
        activeStream: { requestId: action.requestId, assistantId }
      };
    }
    case 'assistant/chunk': {
      if (action.requestId !== state.activeStream?.requestId) return state;
      const id = state.activeStream.assistantId;
      const prev = state.messagesById[id];
      return {
        ...state,
        messagesById: { ...state.messagesById, [id]: { ...prev, content: prev.content + action.chunk } }
      };
    }
    default:
      return state;
  }
}
                  

This pattern avoids out-of-order updates by rejecting stale chunks that do not match the active request id.

Runnable example #2: retry + finalize path

JAVASCRIPT
function finalize(state, requestId) {
  if (requestId !== state.activeStream?.requestId) return state;
  const id = state.activeStream.assistantId;
  const msg = state.messagesById[id];
  return {
    ...state,
    messagesById: { ...state.messagesById, [id]: { ...msg, status: 'done' } },
    activeStream: null
  };
}

function failAndRetry(state, requestId) {
  if (requestId !== state.activeStream?.requestId) return state;
  const id = state.activeStream.assistantId;
  return {
    ...state,
    messagesById: {
      ...state.messagesById,
      [id]: { ...state.messagesById[id], status: 'error', error: 'network_timeout' }
    },
    activeStream: null
  };
}
                  

Keep retry metadata on the failed assistant turn so the user can retry that exact turn without duplicating prior history.

Common pitfalls

      • Appending chunks without request IDs, causing duplicate or out-of-order assistant text.
      • Mutating optimistic messages in place and failing to reconcile when the server returns canonical IDs.
      • Keeping unbounded history in memory and payloads, which slows rendering and increases token cost.

When to use / when not to use

Use local-only ephemeral state for short support chats or anonymous sessions. Use persisted server-backed state for cross-device continuity, analytics, moderation, and reliable retry history. Do not persist everything by default if privacy scope is unclear; define retention and redaction policies first.

Interview follow-ups

Q1: How do you prevent stale chunks after a new prompt? A: Tag every stream with a requestId and discard chunks that do not match current activeStream.
Q2: How do you keep scroll behavior usable? A: Auto-scroll only when user is near bottom; freeze auto-scroll when user reads history.
Q3: How do you handle token limits? A: Maintain rolling summaries/checkpoints and send only recent turns plus summary context.

Implementation checklist / takeaway

Define message state transitions first, enforce request-scoped streaming, then add persistence and truncation rules. Strong answers show reliability under retries, reconnects, and long sessions.

Similar questions
Guides
9 / 61