R1.2 — Where state lives: stateful services, stateless services and hybrid systems
Part of R1. The story in R1 relied on one property: any copy of the payment service could take the request. This part explains where state lives so that this is true, and what to do when it cannot be.
Every service has state. The design question is never "does it have state" — it is where the state lives between requests, and what happens to it when the machine holding it dies.
Stateful: the server remembers
A stateful server keeps the client's state in its own memory between requests: the session, a game position, the open order book.
You pick this when latency is non-negotiable. A multiplayer game updates its match state 50 to 60 times a second; a chat server holds millions of open WebSocket connections; an exchange matching engine matches in microseconds. A network hop to a shared store on every one of those would be the bottleneck.
The price is that the client must keep coming back to the same server. That is sticky sessions (session affinity) at the load balancer, done one of three ways:
| Method | How it works | Catch |
|---|---|---|
| Cookie-based | The load balancer adds a cookie naming the server | New servers get no traffic until new sessions start |
| Session-ID-based | The server's identity is a suffix in the session ID (Tomcat JSESSIONID + jvmRoute) |
The load balancer keeps no table, but you are tightly coupled to the ID format |
| IP-based | Hash the client IP to a server | Clients behind one NAT all land on one server — R14 covers IP hash |
And the drawbacks follow from the pinning:
- Memory and CPU per session, on every server.
- Hard to scale out. A new server takes no load from existing sessions.
- A crash loses every session on that server. The state existed in one place.
- Deployments are complex. You drain sessions before you can restart.
- Replicating sessions to survive crashes brings back every consistency problem in R0.
Stateless: the server keeps nothing between requests
A stateless server keeps nothing in its own memory between one request and the next. Whatever the next request needs about this client — who they are, which step of a flow they are on — comes from somewhere other than the server that happens to receive it.
Any server can serve any request. That one property buys almost everything:
- Scale-out is instant: add a copy, it takes traffic.
- The load balancer can use the simplest algorithms — round robin, least connections.
- It fits containers, autoscaling and serverless, where instances appear and vanish.
- A crashed instance loses nothing but the requests it was in the middle of.
What "stateless" does not mean is "has no data". A stateless payment service writes payments to Postgres on every call, and it is still stateless: the term is about the conversational state between one client's requests — the session — not about whether the system persists anything. The Twelve-Factor App puts it the standard way: processes are stateless and share nothing, and any data that must persist lives in a stateful backing service such as a database.
So for a stateless service the only design question left is where the session lives, and there are two answers:
| Session in the request | Session in a shared store | |
|---|---|---|
| What the client sends | A signed token (JWT) carrying user, roles and expiry | An opaque session ID |
| Work per request | Verify a signature — CPU only, no network call | A lookup in Redis or a database |
| New dependency | None | The store, which must be highly available |
| Logout and revocation | Hard: the token is valid until it expires, so keep it short-lived | Instant: delete the session |
| What it can hold | Small, mostly fixed claims, readable by the client (signed, not encrypted) | Anything, of any size, changing mid-session — a cart, a completed MFA step |
| Typical tools | JWT, OAuth 2.0, OpenID Connect | Spring Session on Redis, DynamoDB, a SQL table |
Both are stateless services. The second does not make the server stateful; it moves the session off the server into a store every instance can reach. Real systems usually combine them: a short-lived JWT so most requests need no lookup, and a refresh token or session record in the store so access can be revoked.
Keeping the session in a shared store
The shared store is where most of the operational cost of a stateless design ends up, so it is worth naming:
| What you gain | What you pay | |
|---|---|---|
| Scaling | Free scale-out; app and store scale independently | Extra latency on every request that needs the session |
| Failure | A server crash loses nothing | The store is a new single point of failure if it has no failover |
| Operations | Easier deploys, nothing to drain | More infrastructure to run; multi-region consistency of the store |
The practices that make it safe: run the store redundantly, back it up, pool connections to it, and put a TTL on every session so abandoned ones expire on their own.
Store choice is the usual trade: Redis gives microsecond latency, native TTL and replication; a cloud-managed Redis removes the operating burden; SQL is durable but slower; MongoDB or DynamoDB give a flexible shape.
Hybrid systems: stateful and stateless services side by side
Stateful and stateless describe a service. "Hybrid" describes a system that runs both kinds deliberately: a large stateless tier that absorbs scale, and a few stateful services where holding state in memory is the whole point.
Start stateless; keep the session in the request or a shared store; add a stateful service only for a clear real-time case, and keep it small, isolated and rebuildable.
The rules that make a hybrid system work:
- Keep the stateful part narrow. It does the one job that needs memory; everything else stays stateless around it.
- Route to it by ownership, not by stickiness. A stateful service owns entities — a match, a chat room, an order book, a document — and requests find the owner through consistent hashing or a registry (R10), not through a sticky cookie at the load balancer.
- Give it a rebuild path. A journal, a checkpoint or a changelog, so a crashed instance comes back with its memory.
Where real systems put their state
The categories are easiest to defend with examples. Learn a few from each table.
Stateless, session in the request or none at all — every instance is interchangeable.
| System | Why stateless works | Where the state actually lives |
|---|---|---|
| Public REST APIs (Stripe, GitHub) | Each call carries an API key or token; any instance can answer | The database; idempotency keys are stored server-side |
| Payment-initiation API | Request in, 202 and a payment ID out; the payment row and idempotency record are the only memory |
Payments database, idempotency table (R3) |
| Payment validation (IBAN/BIC checks, field validation, enrichment) | Request in, verdict out; scale with message volume | Reference data in a database or cache |
| Serverless functions (AWS Lambda) | Instances appear and vanish per request | DynamoDB, S3, queues |
| Image resizing / thumbnail services | A pure function: input to output | Object storage + CDN |
| Search query tier, GraphQL/BFF layers | Fan out to backends, no memory between calls | The search index, downstream services |
Stateless, session or shared counters in a fast store — the servers still hold nothing.
| System | Pattern | Shared store |
|---|---|---|
| Banking web and mobile login | Short-lived stateless JWT access token, plus a server-side refresh token or session so it can be revoked | Redis or a database |
| E-commerce login sessions and carts | Spring Session or a server-side session in a store; any node serves the user | Redis, DynamoDB (Amazon's Dynamo paper was motivated by the shopping cart) |
| Payment workflow orchestration | Stateless workers pick up the next step; workflow state is persisted | Workflow engine database (Temporal, Camunda), outbox tables (R4) |
| API rate limiting | Stateless gateways share counters | Redis INCR + TTL |
| Netflix microservices | Stateless services + a replicated cache for per-member data | EVCache (memcached-based) |
| Ride-hailing driver locations | A stateless API writes GPS pings; matching reads nearby drivers | Redis geo sets |
Stateful — the state in memory is the product.
| System | Why it must be stateful | How it copes with the downsides |
|---|---|---|
| Exchange matching engines (e.g. the LMAX architecture) | The order book is in memory for microsecond matching | Single-threaded engine + event journal; replay to rebuild; hot standby |
| Multiplayer game servers | Authoritative match state updated many times a second | One match per server process; players reconnect to the same server |
| Chat and presence (WhatsApp on Erlang, Discord on Elixir) | Millions of open connections; per-room state in memory | Consistent hashing to place rooms (R10); processes restart and rehydrate |
| Collaborative editing (Google Docs, Figma) | One server holds the live document and merges edits | Per-document routing; periodic checkpoints to storage |
| Video calls (WebRTC SFU media servers) | Per-call media routing state | The call is pinned to one server; clients reconnect on failure |
| Stream processing (Flink, Kafka Streams) | Windows, joins and aggregates kept in local state | Checkpoints and changelog topics for recovery |
Read the third column of the stateful table again. Every stateful system that survives in production has a way to rebuild its memory — a journal, a checkpoint, a changelog. Stateful does not mean "the state exists only in RAM"; it means the hot copy is in RAM and the durable copy is somewhere you can replay from.
Hybrid systems — a stateless tier around a few stateful services.
| System | Stateless services | Stateful services |
|---|---|---|
| WhatsApp, Discord | Profile, contacts and media-upload APIs | Connection and chat servers holding open sockets and room state |
| Exchange or FX trading platform | Order entry, validation and pre-trade risk checks | The matching engine holding the order book |
| Uber | Trip, pricing and payment APIs | In-memory supply and location indexes sharded by geography (Uber built Ringpop to route to them) |
| Google Docs | Document listing, sharing and permissions APIs | One collaboration server per live document, merging edits |
| Online games | Matchmaking, store and profile APIs | Match servers holding authoritative game state |
| Payments platform | Initiation, validation and status APIs | Stream processors keeping rolling fraud features and real-time limit counters (Flink) |
The trap to name: "our login is stateless because we use JWTs." A pure JWT cannot be revoked before it expires, because nobody is asked. Banking logins pair a short-lived JWT with a refresh token held server-side precisely so that a stolen phone can be cut off. The service is still stateless — the revocable part of the session lives in a shared store — and saying so is the senior answer.
Before reading on, step through the walkthrough below. It starts with a stateful server behind sticky sessions, crashes it, and shows the sessions vanishing. Then watch the same traffic served by stateless JWTs, and finally by stateless servers over an external session store — and note which step survives the crash and why.
The defensible position: name the state, then say where it lives. "The payment API is stateless; sessions and refresh tokens are in Redis with a TTL; the matching engine is stateful, journaled, and rebuilt by replay." That sentence describes a hybrid system: stateless services everywhere except the one place where memory is the product.
What to say in the interview
"The payment API is stateless — any instance serves any request, and the state lives in the payments database, the idempotency table and a Redis session store with a TTL. I keep stateful components, like a matching engine, for genuine real-time cases, and journal them so they can be rebuilt after a crash."