Module 01 · Lesson 10 of 14Free
Stateless and stateful services: impact on scaling
- Level
- Intermediate
- Topic
- System design fundamentals
- Reading time
- 18 min
When you finish this page you can
- Identify state that makes requests depend on a particular app instance.
- Explain how shared storage makes app instances replaceable without removing state from the system.
- Compare shared sessions, sticky routing and client-held state.
- Predict what happens to requests and saved data during scaling, restarts and dependency failures.
Builds on 2 lessons
Start with lesson 718 minVideo lesson · coming soon
During a festival rehearsal, Maya adds 2 products to her cart on . The team restarts the app, and her cart becomes empty. Product pages still work because their data lives in PostgreSQL.
Kartly still has its Module 01 setup: one app server with 8 CPU cores and one database. For this lesson’s experiment, the cart exists only in app memory.
The team is considering another app instance, as discussed in Vertical and horizontal scaling. But a second instance would have different memory.
How can Kartly replace or add app instances without losing a customer’s progress?
Kartly adds instance B with exactly the same code as instance A. Maya’s cart exists only in A’s memory. Will B show the same cart?Show answer
Diagram not drawn yet: kartly-local-cart-state.svg
Maya sends Add item to app A, which holds a cart containing 2 products in its own memory. A later Read cart request goes to app B, whose local cart is empty. Both apps can access the product database, but neither stores carts there.
The dependency is on yesterday’s request
After Maya adds a product, something must remember that choice. That remembered information is : data from past activity that affects later behavior.
In our starting implementation, the app keeps a map:
customer 42 → cart containing product 101 and product 205When another request arrives, the app looks up customer 42. This is simple and avoids a storage call. It also makes the customer depend on the lifetime of that particular app instance.
We call this app tier because correct handling of later requests requires state held by a particular instance.
For this lesson, a stateless app tier has a narrower meaning: any ready instance can handle the next request without needing another instance’s private history. Shared storage may still hold plenty of history.
That distinction matters. “Stateless” does not mean “no database”, “no variables” or “no writes”.
itself is described as stateless in RFC 9110. Yet applications using HTTP can maintain state across requests. The protocol label does not tell you where Kartly stores its cart.1
Classify what must survive
For our cart design, ask what must remain available after the app disappears.
| Information | Must survive app replacement? | Placement in our design |
|---|---|---|
| Parsed request body | No | App memory |
| A calculation in progress | No, but its request may fail | App memory |
| Rebuildable product | Not for correctness | Optional app memory |
| Maya’s saved cart | Yes | Shared storage |
| Login record | Yes, during its valid lifetime | Shared storage |
A session is a record that connects later requests to an earlier login. A browser can send an identifier, and an app can retrieve the corresponding record. Django’s server-side session support uses this separation between the ’s identifier and stored session data.2
The useful test is not “does this app use memory?” It is “would losing this memory change a later request’s correct answer?”
For Maya’s cart, the answer is yes. We must change either where the cart lives or how requests find it.
Routing cannot preserve missing data
Suppose the team temporarily runs instances A and B behind a request router. This is an illustrative expansion, not Kartly’s permanent Module 01 setup.
There are two broad fixes. Keep sending Maya to A, or make her cart accessible from either instance.
Should Kartly keep carts local and route customers back, or move carts into shared storage?
Keep local carts and use .
The router remembers which instance a customer used. Existing cart code changes little, but that customer still depends on the selected instance’s memory.
Store carts in PostgreSQL and let any ready instance handle requests.
Each instance retrieves the cart from the same storage system. Requests gain a storage dependency, but replacing the app no longer removes the saved cart.
Why this choice
Sticky routing, also called , binds later requests to a selected instance. AWS Application offer cookie-based stickiness with this behavior.3
That solves normal routing, not data survival. If A loses its only copy of Maya’s cart, directing her to B cannot reconstruct it. This follows directly from our local-memory assumption.
AWS documents that an unhealthy or deregistered sticky target can be replaced by another target. The routing cookie does not transfer the application’s memory.3
Kartly chooses shared storage because saved carts must survive app replacement. PostgreSQL already exists in this scenario, so we do not introduce another storage service without a measured need.
The choice could change for disposable work where restarting the interaction is acceptable. Affinity can also reduce repeated loading of expensive state. But important local state would still need its own recovery design.
Copying carts between app instances is another possible approach. Under that proposed design, an update must reach another copy before it can survive the first instance’s loss. Waiting adds coordination; not waiting leaves a loss window.
For this lesson’s requirements, that is more machinery than using the existing database. Shared storage removes the routing dependency, but now every instance needs the same rules for accessing it.
Shared records make instances replaceable
First, Kartly moves carts out of app memory while it still has one app instance. Only then does the team repeat the two-instance experiment.
This order separates two changes. If cart behavior breaks, the team can investigate the storage move before adding routing differences.
Diagram not drawn yet: kartly-shared-cart-state.svg
Maya sends requests through a router to either app A or app B. Both apps read login sessions and carts from the same PostgreSQL database. Add item through A saves 2 products there; Read cart through B retrieves those same 2 products.
Here is the responsibility split for the experiment.
| Component | Responsibility | Talks to | State it keeps | On failure |
|---|---|---|---|---|
| Request router | Select a ready app | Client, apps | Routing configuration | Requests need a working route |
| App instance | Check identity; run cart rules | Router, database | Temporary request data | Active requests may fail |
| PostgreSQL | Hold shared records | Apps | Sessions and carts | Cart operations become unavailable |
The intended guarantee is limited: replacing an app does not remove records already saved in the database. We are not claiming that this single database survives every possible failure.
One request carries a reference
For the experiment, the browser sends this request over :
GET /v1/cart HTTP/1.1
Host: kartly.example
Cookie: session_id=<opaque-session-id>The identifier is a reference, not the cart and not a customer ID supplied on trust. Any instance uses it to find the session’s customer.
Our illustrative storage layout is:
| Record | Key and fields | Purpose |
|---|---|---|
| Session | Session reference → customer ID, expiry | Identify the caller |
| Cart | Customer ID → saved cart | Find that customer’s cart |
| Cart item | Customer ID + product ID → quantity | Store each selected product once |
The app owns the interpretation of these records. PostgreSQL owns their saved contents. details belong in Sessions and cookies; here we assume a correctly implemented session lookup.
The read path
- The router selects B.
- B retrieves the session record and checks that it is valid.
- B uses the record’s customer ID to retrieve the cart.
- B returns the saved products and quantities.
In our , an invalid session receives 401 Unauthorized. A successful read receives 200 OK. A database receives 503 Service Unavailable, not an invented empty cart.
These are design choices for this example. They distinguish “you need to sign in” from “we cannot read your saved data”.
The write path
- The router selects A for an add-item request.
- A resolves the session and validates the requested product and quantity.
- A asks the database to change the customer’s cart.
- A sends success only after the database reports a successful commit.
A later read through B follows the same lookup path. It does not ask A what happened.
This is why moving only login sessions is insufficient. A shared session can identify Maya on B, but B still cannot display a cart left in A’s memory.
Our design has removed dependence on a particular app. It has not removed dependence on storage, nor solved concurrent changes to the same cart.
Shared state still needs update rules
Imagine Maya uses two tabs. Each reads a quantity of 1, adds one locally, and writes back 2.
If our implementation allows both read–modify–write sequences without coordination, the final quantity can be 2 rather than 3. Both requests found the same cart. The problem is how they changed it.
That failure follows from the proposed update algorithm, not from whether the app is stateless.
Kartly therefore needs the database to perform a coordinated quantity change, rather than accepting a quantity calculated from an earlier read. Transactions and ACID guarantees develops the storage mechanisms.
Retries are a separate problem. Suppose an add-item operation commits, but A disappears before returning success. The browser does not know whether the change happened.
Sending the retry to B is possible now. Whether repeating it adds another item depends on the API’s retry rules. Stateless placement alone gives no duplicate-prevention guarantee.
For our design, distinguish three promises:
- Placement: any ready app can access the cart.
- Concurrent updates: overlapping changes follow a defined rule.
- Retries: repeating an uncertain operation has a defined effect.
Only the first promise follows from moving state out of the app. The other two require explicit mechanisms.
That leaves another placement choice: could the browser carry the state instead of looking it up?
Client-held state changes the costs
A request can carry more than an identifier. For example, a signed cookie can carry session information.
A signature lets the server detect changed contents. It does not hide those contents or prove that the client returned the newest valid value. Django’s signed-cookie documentation makes both limitations explicit.2
Should Kartly put the mutable cart in a signed cookie instead of shared storage?
Carry the cart in a signed cookie.
An instance can read the submitted cart without retrieving a cart record. But our design must resolve competing browser copies and decide whether older valid carts may be replayed.
Keep the cart in shared storage.
Requests carry a reference. The database remains the common record for every tab and device, at the cost of a storage call.
Why this choice
Consider two tabs receiving different cart contents. Under a client-held design, each later request can bring a different version. A valid signature alone cannot tell the server which version should win.
Our product requirement is one current cart across devices. We choose a shared record because it gives those devices one place to read and update.
Client-held state is more attractive for small, short-lived information where accepting an older valid value is harmless. A display preference is a better fit than our mutable cart.
Moving login claims into a signed token would also be a separate decision. It would not move the cart, inventory or orders out of the database.
Client-held state can remove a lookup for particular data. It does not make every kind of shared business state unnecessary.
Replacement still interrupts active work
Now the team restarts A after Maya’s cart has been saved. B can read that cart because the app restart does not change our database records.
But a request executing inside A can still be interrupted. Replaceable instances are not interruption-free instances.
For planned deployments, our operating procedure is to stop sending new requests to A, let active requests finish, then stop it. This is called draining. AWS documents a deregistration delay for in-flight requests and warns about errors when targets terminate connections early.3
The same distinction applies to local files. If our app returns “upload saved” before moving a file off its temporary disk, replacement can lose the only copy. A cart can be shared correctly while another feature still depends on local storage.
Make failures distinguishable
For Kartly’s proposed design, the operating checks should match what customers could lose.
| Failure | What Maya sees | Detection and recovery |
|---|---|---|
| App disappears | A request may fail | Detect failed instance; route later requests elsewhere |
| Database is unavailable | Cart temporarily unavailable | Track storage errors; restore storage access |
| Session is expired | Sign-in required | Check expiry; establish a new valid session |
| Storage times out | Uncertain write result | Record timeout; resolve outcome before an unsafe repeat |
A crucial rule: do not interpret “storage could not answer” as “there is no saved cart”. Otherwise an outage can become a misleading empty-cart response.
Adding app instances also increases possible demand on the same database. Under our design, each cart request still needs shared storage. More app capacity does not automatically add storage capacity.
We have moved responsibility, not removed it. The next question is how much state we may safely treat as disposable.
Going deeper
Caches, connections and the meaning of stateless
A does not make an app stateful in the important sense if losing it changes only performance. For Kartly, a missing cache entry must be recoverable from the shared record. Cache freshness is a separate correctness question.
Session storage needs an explicit loss policy. Django warns that cache-only session data can disappear through eviction or restart, logging users out. Its documentation describes a database-backed cache alternative.2
Under our cart requirements, losing a login session and losing a cart have different costs. Reauthentication may recover access; it cannot reconstruct products that were never saved elsewhere.
Open connections are another boundary. A is a long-lived, two-way connection, and AWS documents that an upgraded connection stays with its selected target. Replacing that target still needs a reconnect strategy.3
Thus a service can have externally stored business state and still have connection state tied to one running instance.
Common mistakes
- Calling the entire system stateless. Name the boundary: our app tier is replaceable; its storage tier keeps the records.
- Treating sticky routing as a backup. Routing to A cannot recover memory that A has lost.
- Returning an empty cart on timeout. An unavailable record and an absent record are different outcomes.
- Moving only the obvious state. Check sessions, carts, temporary files and unfinished work before replacing an instance.
- Expecting storage placement to solve races. Shared access still needs update and retry rules.
Where this shows up
- Design: a URL shortener for millions of users: redirect instances need access to mappings created elsewhere.
- Design: a fast product catalog page: local cached data must not become the only authoritative copy.
Kartly can now replace an app instance, but the next lesson asks whether its features should remain together in one application or several services.
Check your understanding
An app keeps parsed templates in memory, but can rebuild them from its deployed files. Does this prevent the app tier from being stateless?Show answer
Maya’s login works on B, but her cart appears empty there. Sessions are shared. What should you inspect next?Show answer
Sticky routing sends Maya to A. A crashes and loses its only cart copy. Can routing her to B restore the cart?Show answer
A saves a cart change, then crashes before replying. B handles the retry. Does a stateless app tier guarantee that the item is added only once?Show answer
The shared database times out during a cart read. Should the app return an empty cart because it found no result?Show answer
Summary
- Keep required cross-request state outside a particular app instance, so another instance can retrieve it after scaling or replacement.
- Stateless describes an app boundary, not a system without memory, writes or databases.
- Sticky routing preserves affinity, not the data lost with a failed instance.
- Shared storage introduces a dependency; it does not automatically solve concurrent updates, retries or database failures.
- Replaceable apps still need draining, clear failure responses and recovery rules for active work.