lumo

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

  1. Identify state that makes requests depend on a particular app instance.
  2. Explain how shared storage makes app instances replaceable without removing state from the system.
  3. Compare shared sessions, sticky routing and client-held state.
  4. Predict what happens to requests and saved data during scaling, restarts and dependency failures.

Builds on 2 lessons

Start with lesson 718 min
  1. Core system properties: availability, durability, consistency18 min
  2. Vertical and horizontal scaling: when to use each14 min

Video 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?

Check yourselfAnswer in your head first, then reveal
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 answerHide answer
No. Identical code does not mean shared memory. B needs either the cart itself or a way to retrieve it from somewhere both instances can access.

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.

Copying the app does not copy the customer’s progress. The cart belongs to A’s memory, not to the running code.

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:

text
customer 42 → cart containing product 101 and product 205

When 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.

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.

The changed part is state ownership: PostgreSQL holds the saved cart. A and B keep only temporary request data.

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 :

http
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

  1. The router selects B.
  2. B retrieves the session record and checks that it is valid.
  3. B uses the record’s customer ID to retrieve the cart.
  4. 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

  1. The router selects A for an add-item request.
  2. A resolves the session and validates the requested product and quantity.
  3. A asks the database to change the customer’s cart.
  4. 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.

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

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

Check yourselfAnswer in your head first, then reveal
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 answerHide answer
No. Losing those templates changes startup cost, not the correct result. The important dependency is irreplaceable history, not the existence of memory.
Check yourselfAnswer in your head first, then reveal
Maya’s login works on B, but her cart appears empty there. Sessions are shared. What should you inspect next?Show answerHide answer
Inspect where cart data lives. Sharing identity does not share every other record; the cart may still exist only in A’s memory.
Check yourselfAnswer in your head first, then reveal
Sticky routing sends Maya to A. A crashes and loses its only cart copy. Can routing her to B restore the cart?Show answerHide answer
No. Not the router: it selects a destination but does not reconstruct application data. Recovery requires another surviving copy or a saved record.
Check yourselfAnswer in your head first, then reveal
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 answerHide answer
No. Both instances can access the cart, but repeating the operation may apply it again. The API needs a separate rule for uncertain retries.
Check yourselfAnswer in your head first, then reveal
The shared database times out during a cart read. Should the app return an empty cart because it found no result?Show answerHide answer
No. A timeout is not evidence that the cart is absent. Return temporary unavailability rather than inventing a successful empty result.

Summary

  1. Keep required cross-request state outside a particular app instance, so another instance can retrieve it after scaling or replacement.
  2. Stateless describes an app boundary, not a system without memory, writes or databases.
  3. Sticky routing preserves affinity, not the data lost with a failed instance.
  4. Shared storage introduces a dependency; it does not automatically solve concurrent updates, retries or database failures.
  5. Replaceable apps still need draining, clear failure responses and recovery rules for active work.

Report a mistake

Tell us what’s wrong in this lesson and we’ll fix it.