CoursePython · Object-Oriented Design, Provider Abstraction, and Persistence · part 43 of 79
Part 43 · Object-Oriented Design, Provider Abstraction, and Persistence

Lesson 6: Ephemeral State

4 min read·9 Sept 2026

Redis for conversation state that survives restart

The agent from Module 6 holds a conversation in a list of messages. That list lives in process memory, so a restart loses every in-flight conversation, and a second application instance cannot continue a conversation the first one started.

Redis is an in-memory data store used for exactly this: state that must be shared and survive a restart, but does not warrant a relational table.

python
import jsonimport redis.asyncio as redis

class RedisConversationStore:    """Conversation state that survives restarts and is shared across instances."""
    def __init__(self, client: redis.Redis, *, ttl_seconds: int = 3600) -> None:        self._client = client        self._ttl = ttl_seconds
    def _key(self, conversation_id: str) -> str:        return f"conversation:{conversation_id}"
    async def load(self, conversation_id: str) -> list[Message]:        raw = await self._client.get(self._key(conversation_id))        if raw is None:            return []        return [Message(**item) for item in json.loads(raw)]
    async def save(self, conversation_id: str, messages: list[Message]) -> None:        payload = json.dumps([asdict(m) for m in messages])        await self._client.set(self._key(conversation_id), payload, ex=self._ttl)
    async def delete(self, conversation_id: str) -> None:        await self._client.delete(self._key(conversation_id))

[VOLATILE: redis.asyncio is the current async client in redis-py, having replaced the separate aioredis package. Verify before publishing.]

Three details. Keys are namespaced with a prefix, so conversation:abc cannot collide with cache:abc and you can find or delete all conversations by pattern. JSON is used rather than pickle, for the security reason from Module 4. And every write sets a TTL, which the next section explains.

This is a repository too. Define a ConversationStore Protocol and provide an in-memory implementation for tests, exactly as Lesson 5 did. Redis being fast does not make it acceptable to require it in unit tests.

Redis is not a database. It is memory-first, and depending on its persistence configuration a restart can lose data. Store things you can afford to lose or reconstruct: sessions, caches, rate limit counters, locks. Do not store the only copy of anything that matters.

TTLs, eviction, and what should never be cached

A TTL is an expiry time after which a key is deleted automatically.

python
await client.set("conversation:abc", payload, ex=3600)        # one hour

Setting a TTL on everything is the default habit to build, because the alternative is a store that grows until it fails. Module 2's problem statement mentioned a cache that is a module-level dictionary growing until the process dies, and Redis without TTLs is the same failure with more steps.

Eviction is what happens when Redis reaches its memory limit. The policy is configured on the server, and the two worth knowing are allkeys-lru, which evicts the least recently used key of any kind, and volatile-lru, which evicts only keys that have a TTL set. The second is safer when the same instance holds both cache entries and state you would rather not lose, because it means an untagged key is never evicted to make room.

The practical consequence: any key can disappear at any time. Code reading from Redis must handle a miss, and load above returning an empty list rather than raising is that handling.

What should never be cached or stored here.

Anything whose only copy this would be. If losing it means data loss, it belongs in the database.

Secrets and credentials, unless the store is encrypted and access controlled, since Redis is frequently deployed with weak authentication inside a trusted network, and "inside the network" is a weaker boundary than it sounds.

Personal data without a retention policy, because a TTL is your retention mechanism and an unbounded key holding user content is a compliance problem waiting to be discovered.

Anything where a stale answer causes real harm. A cached permission check is the standard example: revoking access does not take effect until the entry expires, so the cache must be invalidated on revocation rather than left to expire.

Results derived from data that changes more often than the TTL, which produces answers that are confidently wrong.

Cache invalidation. A TTL is expiry, not invalidation. When the underlying data changes, delete the key explicitly:

python
async def update_document(self, document: Document) -> None:    await self._repository.save(document)    await self._cache.delete(f"document:{document.id}")

Note the order: write first, then invalidate. Invalidating before writing leaves a window in which a concurrent reader repopulates the cache with the old value, which then persists for the full TTL.