Designing a Conversation Memory Store

AILLMArchitectureBackend
Share on LinkedIn

The support bot forgot what the customer said three messages ago. Not because the model has amnesia — because someone stored the session in a process-local variable and the next request hit a different pod. Conversation memory is infrastructure, not a prompt trick. Get the store wrong and no amount of prompt engineering fixes the user experience.

Memory tiers

Production apps need at least three tiers:

Tier Contents Storage TTL
Working Last N turns, tool outputs Redis Session lifetime
Episodic Full transcript, metadata Postgres Months–years
Semantic Summaries, extracted facts, embeddings Postgres + vector index Permanent

Working memory feeds the model on every turn. Episodic memory is the audit trail. Semantic memory is what you retrieve when the user says "remember when we talked about pricing?"

Schema that doesn't paint you into a corner

CREATE TABLE conversations (
  id          UUID PRIMARY KEY,
  tenant_id   UUID NOT NULL,
  user_id     UUID NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  metadata    JSONB DEFAULT '{}'
);

CREATE TABLE turns (
  id              UUID PRIMARY KEY,
  conversation_id UUID REFERENCES conversations(id),
  role            TEXT NOT NULL CHECK (role IN ('user','assistant','tool','system')),
  content         TEXT NOT NULL,
  token_count     INT,
  tool_calls      JSONB,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX turns_conv_created ON turns(conversation_id, created_at);

Store tool calls separately from assistant text — you'll need them for debugging and replay, but they bloat context if sent verbatim every turn.

Add a summaries table for rolling compression:

CREATE TABLE conversation_summaries (
  conversation_id UUID PRIMARY KEY REFERENCES conversations(id),
  summary         TEXT NOT NULL,
  covers_through  UUID REFERENCES turns(id),  -- last turn included
  updated_at      TIMESTAMPTZ NOT NULL
);

Context assembly

The orchestrator builds the prompt context — not the client, not the model:

async def build_context(conversation_id: str, budget: int) -> list[Message]:
    summary = await store.get_summary(conversation_id)
    recent = await store.get_recent_turns(conversation_id, limit=8)
    recalled = await store.semantic_recall(conversation_id, recent[-1].content, k=3)

    messages = [SystemMessage(SYSTEM_PROMPT)]
    if summary:
        messages.append(SystemMessage(f"Prior context: {summary}"))
    for chunk in recalled:
        messages.append(SystemMessage(f"Recalled: {chunk.text}"))
    messages.extend(recent)
    return trim_to_budget(messages, budget)

Order matters: system → summary → recalled facts → recent turns. Recalled chunks before recent turns prevents the model from overweighting stale information at the end of context.

Summarization triggers

Don't summarize every turn — it's expensive and lossy. Trigger on:

async def maybe_summarize(conversation_id: str) -> None:
    tokens = await store.count_tokens_since_summary(conversation_id)
    if tokens < SUMMARIZE_THRESHOLD:
        return
    turns = await store.get_unsummarized_turns(conversation_id)
    new_summary = await summarizer.merge(
        existing=await store.get_summary(conversation_id),
        turns=turns,
    )
    await store.save_summary(conversation_id, new_summary, turns[-1].id)

Use a cheap model for summarization. The summary doesn't need to be literary — it needs to preserve decisions, names, numbers, and open questions.

Semantic recall

Embed conversation segments (summary chunks or turn groups) into your vector index scoped by tenant_id and user_id:

async def semantic_recall(conversation_id: str, query: str, k: int) -> list[Chunk]:
    embedding = await embed(query)
    return await vector_db.search(
        embedding,
        filter={"conversation_id": conversation_id},
        top_k=k,
    )

Scope carefully. A recall query that searches all tenant conversations is a data leak waiting for a missing filter clause.

Multi-device and concurrent sessions

Users open the same chat on phone and desktop. Options:

I've seen the single-session model break on WebSocket reconnects. Store a client_message_id for deduplication.

Privacy and retention

Memory stores accumulate PII fast. Build in:

GDPR deletion means cascading deletes across turns, summaries, and vector embeddings — test this path before a legal request arrives.

Common production mistakes

Teams get app conversation memory store wrong in predictable ways:

LLM features around app conversation memory store break in production when prompts assume deterministic output, context windows are sized for dev datasets, or token costs are never budgeted per user session. Always log prompt hash, model version, and latency—not raw prompts with PII.

Debugging and triage workflow

When app conversation memory store misbehaves in production, work top-down instead of guessing:

  1. Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
  2. Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
  3. Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
  4. Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
  5. Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
  6. Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.

Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.

Resources

Frequently asked questions

Should conversation history live in the database or a cache?

Both. Redis or similar for hot session state (last N turns, fast reads on every request). Postgres for durable history, search, and analytics. On session end or every K turns, flush from cache to durable storage. Never rely on cache alone — evictions happen.

How many past turns should I send to the model?

Send the last 4–8 turns verbatim plus a rolling summary of everything before. Exact count depends on turn length and your context budget. Measure: if dropping from 10 to 6 verbatim turns doesn't change eval scores, you're wasting tokens.

When do I need vector search over past conversations?

When users reference things from weeks ago ('like we discussed last month about the API change'). Keyword search handles explicit references; vector recall handles paraphrased callbacks. Most support bots need this by month two.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →