00:00

Stateful vs Stateless Architecture in System Design

Last updated:

A stateless component does not remember anything about a client between requests: every request carries all the information needed to handle it. A stateful component keeps information about clients or ongoing work in its own memory or storage and relies on it for later requests. This single design choice decides how easily a system can scale, recover from failures and be deployed.

This tutorial explains what "state" means, how stateless and stateful components work, the different ways to handle user sessions, a runnable Java example that shows the difference behind a load balancer, how stateful components such as databases are scaled, and how Kubernetes treats each type. It closes with a comparison table, common mistakes and best practices.

1. What Is "State" in a System?

State is any information a system must remember to behave correctly over time. Examples:

  • Who is logged in, and their permissions (session state).
  • The items in a shopping cart before checkout.
  • The step a user has reached in a multi-page form.
  • An open WebSocket connection and the chat rooms it has joined.
  • The data in a database: orders, accounts, products.
  • The position of every player in a multiplayer game.

State itself is unavoidable: every useful application remembers something. The real design question is where the state lives. If it lives in the memory of a particular application server, that server becomes special and hard to replace. If it lives in a dedicated store, the application servers can be identical and disposable.

A helpful analogy: a stateless service is like a vending machine. It does not know who you are; you put in money and press a button, and it gives you a drink. A stateful service is like your personal bank adviser, who remembers your history from previous meetings. If that adviser is absent, the next person has to start from scratch.

2. What Is a Stateless Architecture?

In a stateless architecture, application servers keep no client-specific data in memory between requests. Each request includes everything needed to process it, such as an authentication token and the relevant IDs, and any data that must persist is read from and written to an external store.

Stateless servers behind a load balancer A user's three requests are sent by a load balancer to three different servers. None of the servers keeps the user's data; all of them read and write the cart in a shared store such as Redis or a database, so every request sees the same cart. User Load Balancer Server 1request 1 · keeps nothing Server 2request 2 · keeps nothing Server 3request 3 · keeps nothing Shared store Redis / database cart: keyboard, mouse
Figure 1: Stateless servers are interchangeable. Any server can handle any request because the state lives in a shared store.

Because every server is identical, you can:

  • Scale horizontally by simply adding more servers.
  • Balance load freely, using any algorithm, without tying users to servers.
  • Survive failures: if a server crashes, the load balancer sends the next request elsewhere and nothing is lost.
  • Deploy easily: replace servers one by one during a rolling update without logging anyone out.

Typical stateless components include REST APIs authenticated with tokens, web front-end servers, API gateways, image resizing services and serverless functions. Note that a stateless service can still use a database; it simply does not keep client data in its own memory.

3. What Is a Stateful Architecture?

In a stateful architecture, a server keeps data about a client or a task in its own memory, and later requests depend on that data. The classic example is a web application that stores the user's login session and cart in the server's memory (an in-memory HTTP session).

Because the data exists on only one server, the user's requests must keep returning to that server. Load balancers do this with sticky sessions (session affinity), usually based on a cookie.

A stateful server with sticky sessions A user is pinned by a sticky load balancer to Server A, which holds the user's session and cart in memory. Server B holds nothing for this user. When Server A crashes, the session and cart are lost and the user is sent to Server B, which does not recognise them. User Load Balancer sticky sessions Server A · crashed session + cart in memory lost with the server Server B "who are you?" pinned here failover user logged out, cart emptied uneven load and harder deployments
Figure 2: With stateful servers, a user is pinned to one server. If that server fails or is redeployed, the user's session and in-progress data disappear with it.

Stateful designs have real advantages in some situations: data in local memory is extremely fast to access, and long-lived connections (WebSockets, game sessions, streaming) are naturally stateful. But for ordinary web request handling, keeping state in application servers creates problems:

  • Lost data on failure: a crash or restart wipes in-memory sessions.
  • Uneven load: sticky sessions can leave some servers overloaded while others sit idle.
  • Harder scaling: new servers only receive new users; existing users stay pinned to old servers.
  • Painful deployments: every rolling update risks logging users out unless sessions are drained carefully.

4. Stateful vs Stateless: Comparison Table

AspectStatelessStateful
Client data on the serverNone kept between requestsKept in memory or local storage
What each request containsEverything needed (token, IDs, parameters)Often just a session reference
Horizontal scalingEasy: add identical serversHarder: data must be partitioned or replicated
Load balancingAny algorithm, any serverSticky sessions or state-aware routing
Failure impactRetry on another server, nothing lostState on the failed server may be lost
DeploymentsSimple rolling updatesNeed draining, migration or replication
PerformanceMay need a lookup in an external storeFast local access to state
ComplexityLower for the service; state moves to dedicated storesHigher: replication, consistency, recovery
ExamplesREST APIs, web servers, serverless functions, gatewaysDatabases, caches, message brokers, WebSocket and game servers

5. Seeing the Difference in Java

The program below simulates two servers behind a round-robin load balancer. A user adds three items to a cart, and each request lands on the next server. First the servers keep carts in their own memory (stateful), then they use a shared store (stateless):

StatefulVsStatelessDemo.java
package com.javabytechie.state;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** Two servers behind a round-robin load balancer: carts kept in server memory vs in a shared store. */
public class StatefulVsStatelessDemo {

    interface CartServer {
        List<String> addItem(String sessionId, String item);
    }

    /** Stateful: each server remembers carts in its own memory. */
    static class StatefulServer implements CartServer {
        private final Map<String, List<String>> carts = new HashMap<>();
        public List<String> addItem(String sessionId, String item) {
            List<String> cart = carts.computeIfAbsent(sessionId, k -> new ArrayList<>());
            cart.add(item);
            return cart;
        }
    }

    /** Stateless: servers keep nothing; carts live in a shared store (Redis or a database in real life). */
    static class StatelessServer implements CartServer {
        private final Map<String, List<String>> sharedStore;
        StatelessServer(Map<String, List<String>> sharedStore) { this.sharedStore = sharedStore; }
        public List<String> addItem(String sessionId, String item) {
            List<String> cart = sharedStore.computeIfAbsent(sessionId, k -> new ArrayList<>());
            cart.add(item);
            return cart;
        }
    }

    static void shop(String title, CartServer[] servers) {
        System.out.println(title);
        String[] items = {"keyboard", "mouse", "monitor"};
        for (int i = 0; i < items.length; i++) {
            CartServer server = servers[i % servers.length];          // round-robin load balancing
            List<String> cart = server.addItem("session-42", items[i]);
            System.out.println("  request " + (i + 1) + " -> server " + (i % servers.length + 1) + " : cart = " + cart);
        }
    }

    public static void main(String[] args) {
        shop("STATEFUL servers (cart in server memory)", new CartServer[] { new StatefulServer(), new StatefulServer() });
        Map<String, List<String>> redis = new HashMap<>();
        shop("STATELESS servers (cart in shared store)", new CartServer[] { new StatelessServer(redis), new StatelessServer(redis) });
    }
}
Output
STATEFUL servers (cart in server memory)
  request 1 -> server 1 : cart = [keyboard]
  request 2 -> server 2 : cart = [mouse]
  request 3 -> server 1 : cart = [keyboard, monitor]
STATELESS servers (cart in shared store)
  request 1 -> server 1 : cart = [keyboard]
  request 2 -> server 2 : cart = [keyboard, mouse]
  request 3 -> server 1 : cart = [keyboard, mouse, monitor]

With stateful servers, the user's cart is split across two machines: server 2 never saw the keyboard, so the user's view jumps around between requests. With stateless servers sharing one store, every request sees the complete cart, no matter which server handles it. This is exactly the bug teams hit when they add a second server to an application that keeps sessions in memory.

6. Handling User Sessions: Four Approaches

User sessions are the most common source of unwanted state in web applications. There are four main ways to handle them:

ApproachHow it worksProsCons
In-memory session (stateful)Session lives in the app server's memorySimplest, fastestLost on restart; needs sticky sessions
Sticky sessionsLoad balancer pins each user to one serverEasy to add to existing appsUneven load; still loses sessions on failure
Shared session storeSessions kept in Redis or a database, keyed by a session cookieServers become stateless; sessions survive restarts; can revoke sessionsExtra network hop; the store must be highly available
Self-contained tokens (JWT)Signed token sent by the client on every request contains the user's identity and claimsNo server-side session storage at allHard to revoke before expiry; keep tokens short-lived and small

For most applications, a shared session store or short-lived tokens are the right choice. Both make the application tier stateless while still remembering who the user is.

7. Making a Spring Boot Application Stateless

Option 1: Move HTTP sessions to Redis with Spring Session. Your code keeps using HttpSession as before, but the data is stored in Redis instead of server memory. Add the spring-session-data-redis and spring-boot-starter-data-redis dependencies, then configure the connection:

application.yml
spring:
  data:
    redis:
      host: redis.internal
      port: 6379
  session:
    timeout: 30m          # sessions expire after 30 minutes of inactivity
server:
  servlet:
    session:
      cookie:
        secure: true
        http-only: true
        same-site: lax
CartController.java
package com.javabytechie.state;

import java.util.ArrayList;
import java.util.List;

import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CartController {

    @PostMapping("/api/cart/items")
    @SuppressWarnings("unchecked")
    public List<String> addItem(@RequestParam String item, HttpSession session) {
        // With Spring Session, this session is loaded from and saved to Redis,
        // so any server instance sees the same cart.
        List<String> cart = (List<String>) session.getAttribute("cart");
        if (cart == null) {
            cart = new ArrayList<>();
        }
        cart.add(item);
        session.setAttribute("cart", cart);   // write back so the change is persisted
        return cart;
    }
}

Option 2: Fully stateless APIs with tokens. For REST APIs used by mobile apps and single-page applications, tell Spring Security never to create a session, and authenticate every request with a bearer token:

SecurityConfig.java
package com.javabytechie.state;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        http
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable())                        // no cookies, so no CSRF risk for this API
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));   // validate the JWT on every request
        return http.build();
    }
}

Business data such as carts and orders still belongs in a database or cache, not in the token. Tokens should carry only identity and permissions.

8. Where Stateful Components Belong

The common architecture pattern is to push all state down into a small number of specialised, carefully managed stateful components, and keep everything above them stateless:

Stateless application tier on top of a stateful data tier The top tier contains stateless components: web servers, API services and workers, which can be added or removed freely. The bottom tier contains stateful components: a database, a cache and a message broker, each replicated and managed carefully. STATELESS TIER · SCALE FREELY, RESTART ANYTIME Web servers API services Background workers STATEFUL TIER · REPLICATED, BACKED UP, MANAGED CAREFULLY Database Cache / session store Message broker
Figure 3: Keep the application tier stateless so it can scale and restart freely, and concentrate state in specialised components that are built to replicate and recover it.

Stateful components cannot simply be copied, because every copy must hold the right data. They are scaled and protected with dedicated techniques:

  • Replication: keep copies of the data on several nodes for availability and read capacity. See Database Replication.
  • Partitioning (sharding): split the data so each node holds a portion. See Database Sharding.
  • Consensus and leader election: make sure nodes agree on the current state and who may write it.
  • Backups and point-in-time recovery: protect against mistakes and corruption.
  • Managed services: cloud databases, caches and brokers handle much of this complexity for you.

Some application components are stateful by nature, such as WebSocket servers holding live connections, multiplayer game servers and stream processors. For these, keep durable data in shared stores, make clients reconnect gracefully, and use consistent routing so a client returns to the right node.

9. Stateless and Stateful Protocols

ProtocolTypeWhy
HTTPStatelessEach request is independent; cookies and tokens add state on top when needed
DNS (over UDP)StatelessEach query and answer stands alone
TCPStatefulBoth ends track sequence numbers and connection state
WebSocketStatefulA long-lived connection with context on both sides
FTPStatefulThe server remembers the logged-in user and current directory

HTTP being stateless is one of the main reasons the web scales so well: any server can answer any request.

10. Stateless and Stateful Workloads in Kubernetes

Kubernetes has separate building blocks for the two types:

  • A Deployment runs identical, interchangeable pods. Pods get random names and can be replaced or rescheduled anywhere. It is designed for stateless services.
  • A StatefulSet gives each pod a stable name (db-0, db-1), a stable network identity and its own persistent volume that follows it if the pod is rescheduled. It starts and stops pods in order. It is designed for databases, brokers and other stateful systems.
statefulset.yaml (simplified)
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: orders-db
spec:
  serviceName: orders-db
  replicas: 3
  selector:
    matchLabels:
      app: orders-db
  template:
    metadata:
      labels:
        app: orders-db
    spec:
      containers:
        - name: postgres
          image: postgres:16
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:          # each pod (orders-db-0, -1, -2) gets its own persistent disk
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi

Running databases yourself on Kubernetes is possible but demanding; many teams prefer managed database services and keep Kubernetes for stateless workloads.

11. When to Choose Each

Make it stateless (the default for application code):

  • REST and GraphQL APIs, web front ends and API gateways.
  • Any service you want to auto-scale, deploy frequently or run on serverless platforms.
  • Workers that process jobs from a queue.

Accept statefulness where it is the job itself:

  • Databases, caches, search engines and message brokers.
  • Real-time connections: WebSockets, multiplayer games, video calls.
  • Stream processing that keeps running aggregates, and workflows that track long-running progress.

12. Common Mistakes to Avoid

  • Keeping sessions in server memory and only discovering the problem when a second server is added.
  • Relying on sticky sessions as the fix, which still loses sessions on failure and causes uneven load.
  • Storing uploads or generated files on the local disk of an application server; use object storage instead.
  • In-memory caches holding authoritative data rather than copies that can be rebuilt.
  • Putting large or sensitive data in JWTs, or making tokens long-lived and impossible to revoke.
  • Making the shared session store a single point of failure; it needs replication and monitoring like any database.
  • Scheduled jobs that assume a single server, which then run multiple times when the application scales out.

13. Best Practices

  • Design application services to be stateless by default, and treat servers as disposable.
  • Store sessions in a shared, replicated store, or use short-lived signed tokens.
  • Keep durable data in databases and files in object storage, never on local disks of app servers.
  • Concentrate state in a few specialised components, preferably managed services, and invest in their replication and backups.
  • Make requests idempotent so they can safely be retried on another server.
  • For naturally stateful services, support graceful reconnection and route clients consistently.
  • Use distributed locks or a scheduler with leader election for jobs that must run once across many instances.

14. Summary

  • State is information a system must remember; the key decision is where it lives.
  • Stateless servers keep no client data between requests, so they scale, balance load, recover and deploy easily.
  • Stateful servers keep data locally, which is fast but complicates scaling, failover and deployments.
  • Handle sessions with a shared store or signed tokens rather than server memory and sticky sessions.
  • Keep the application tier stateless and concentrate state in replicated databases, caches and brokers; use Kubernetes Deployments and StatefulSets accordingly.

"Stateless services, stateful stores" is one of the simplest and most effective rules in system design. Open the FAQ tab for answers to common questions about stateful and stateless architecture.