Scalable Session Management for Java Microservices With Valkey or Redis

Published on
June 22, 2026

For modern enterprise applications, three things are non-negotiable: performance, reliability, and continuous availability. Users expect nothing less, and when apps fail to meet these baselines, the results are instantly noticeable. A user may be unexpectedly prompted to log in again, or the app may suddenly lose their history. Maintaining the expected levels of performance, availability, and user experience all comes down to proper session management.

In older applications built on the traditional monolithic architecture, managing sessions was relatively straightforward. All relevant data was stored centrally or hardcoded into the app's workflow logic. However, that architecture proved incapable of scaling to the needs of today's enterprises, ushering in the shift toward apps built around distributed microservices. Session management is not so simple once you put a load balancer in front of multiple application instances distributed across the globe. The moment a request lands on a node that didn't create a session, the user is logged out, their history is now missing, and your supposed "stateless" architecture is revealed to be, in fact, highly stateful.

Scalable session management is possible for Java apps built on microservices. With either the open-source in-memory data store Valkey or Redis, and the Java client Redisson, engineering teams can easily build resilient and scalable architectures that deliver the expected user experience while meeting business continuity goals.

Why In-Memory and Sticky Sessions Fail at Scale

Before digging into why Valkey or Redis and the Redisson client are the ideal solution for Java developers, it helps to understand why and how other approaches to session management fall short.

By default, the standard Java HttpSession lives entirely within the heap memory of a single Java Virtual Machine (JVM). This setup works fine in a single-server deployment. However, today's enterprise systems require redundancy and scale. The moment you add a second instance behind a load balancer, you create two independent session stores that share no information and know nothing about each other.

Perhaps the most common fix for this problem is implementing session affinity, otherwise known as sticky sessions. In this model, the load balancer is configured to pin a specific client to the exact node that originally generated their session. While this provides a temporary illusion of functionality, it soon runs into architectural problems. Sticky sessions can significantly reduce the efficiency of a load balancer. Long-lived sessions concentrate heavy traffic on whichever nodes happened to receive the earliest users. When traffic spikes, autoscaling mechanisms cannot rebalance existing sessions, leading to overloaded nodes and wasted compute resources on idle nodes.

In addition, Continuous Integration/Continuous Deployment (CI/CD) pipelines require rolling restarts and scale-down events. In a sticky session environment, terminating a node immediately evicts all users pinned to it. The result can be catastrophic in a business environment, as users are abruptly logged out mid-task or lose their work history.

The whole point of load balancers and distributed applications is to provide optimal performance to all users regardless of location, while maintaining enterprise-grade availability. But sticky sessions inherently have no fault tolerance. A crashed application node takes all of its local sessions down with it, because there is no fault tolerance. And since session affinity routes a user to a single instance of a service, sticky sessions are essentially incompatible with the microservices architecture.

The Scalable Solution: An Architecture Built on Spring Session and Redisson

Given these limitations, it's clear that today's enterprise applications must move session states out of local JVM memory and into a highly available, high-performance data store. Valkey and Redis have emerged as the industry standards because they provide sub-millisecond in-memory latency, native mechanisms for key expiration, and robust replication capabilities that help ensure high availability.

For Java development teams that utilize the Spring ecosystem, implementing such an architecture is remarkably simple. Spring Session is designed to transparently replace the standard web container's HttpSession implementation with one backed by an external store — no changes to your existing controllers or business logic required.

Redisson sits in the middle, connecting Spring Session to Valkey or Redis. All it takes is the RedissonConnectionFactory object to directly implement Spring Data's RedisConnectionFactory interface. Spring Session runs entirely on Redisson's optimized connection pool and native cluster support.

Dependency Configuration

To get started, Java developers only need to add three dependencies to their build configuration: Spring Web (to supply the controllers), Spring Session's Redis module (which provides the crucial @EnableRedisHttpSession annotation), and the Redisson Spring Boot starter. The appropriate Spring versions are automatically managed by the Spring Boot Bill of Materials (BOM). You can replace org.redisson with pro.redisson if you use Redisson PRO edition.

Here's how to add the dependencies:


    org.springframework.boot
    spring-boot-starter-web



    org.springframework.session
    spring-session-data-redis



    org.redisson
    redisson-spring-boot-starter
    4.3.1

Application Configuration

Configuration in the Java app is equally simple. For a standard Servlet-based Spring MVC application, only a single configuration class is required:

@Configuration
@EnableRedisHttpSession
public class SessionConfig {
  // The starter auto-configures RedissonConnectionFactory.
  // Use @EnableRedisWebSession instead for a reactive (WebFlux) app.
}

Next, in application.yaml, configure Spring Boot to route session storage through Redisson by mapping it to a localized redisson.yaml file:

# application.yml
spring:
  session:
    store-type: redis
    timeout: 30m
  data:
    redis:
      redisson:
        file: classpath:redisson.yaml

# A separate file named redisson.yaml — This is for testing a single node; use clusterServersConfig in production
singleServerConfig:
  address: "redis://valkey-host:6379"
  password: "${REDIS_PASSWORD}"

Spring Security's context, temporary flash attributes, and any explicit session.setAttribute(...) calls will automatically serialize directly into Valkey or Redis. Developers can verify the architecture by restarting the app and routing requests to a different instance. The session data will seamlessly survive the transition.

Lifecycle Management: TTL and Eviction Policies

While maintaining sessions resiliently across nodes is important for security reasons, a session store must also have an expiration strategy.

The specific mechanism Spring Session uses to expire and clean up sessions depends on the configured repository. In current Spring Boot distributions, the standard is the highly efficient RedisSessionRepository. Under this default, setting expiration policies is very straightforward: All you need to remember is that the designated session timeout directly dictates the Time-To-Live (TTL) of the underlying key.

By defining the spring.session.timeout property (or standard server.servlet.session.timeout), you establish the acceptable idle window. With every inbound user request, Spring Session automatically refreshes this TTL. If the user goes completely quiet, Valkey or Redis will natively evict the key once the timer expires. This method requires no server-side configuration, a major advantage when dealing with fully managed cloud databases, which are often locked down by the provider.

Advanced workflows may require explicit expiration events or the ability to query sessions by specific user IDs. This is necessary for executing HttpSessionListener callbacks, forcefully closing associated WebSocket connections upon expiry, or executing a global "log out everywhere" command for a specific user. This functionality requires the RedisIndexedSessionRepository, activated via @EnableRedisIndexedHttpSession. It constructs a secondary index and relies heavily on database keyspace notifications, which are disabled by default in Valkey and Redis. To enable these events, include the following in your valkey.conf or redis.conf file:

# redis.conf / valkey.conf — E (keyevent), g (generic), x (expired)
notify-keyspace-events Egx

If your managed backend prevents Spring Session from automatically asserting this configuration, manually register a ConfigureRedisAction.NO_OP bean and enable the notification policies directly on the server. For the vast majority of stateless microservices, however, the default repository, combined with a standard TTL, works equally well.

Bridging the Microservices Divide: Gateways and Shared State

The real value of externalized sessions becomes evident in more complex microservices topologies. Because every downstream service targets the same Valkey or Redis backend, establishing a shared session state is simply a matter of standardizing how the session ID is transmitted over the network.

For standard web traffic originating from a browser, the session ID is securely stored in an HTTP cookie. When an API gateway, such as Spring Cloud Gateway, receives this cookie, it forwards it to downstream microservices. As long as each downstream service shares the same Spring Session configuration, they will successfully resolve the identical key in the database.

It's important to namespace these keys to prevent collisions between unrelated applications sharing the same database cluster. This has the added benefit of grouping related services. Here's a basic namespace config you can work off of:

spring:
  session:
    redis:
      namespace: "myplatform:sessions"

When it comes to service-to-service communication or token-based API traffic, cookies typically aren't robust enough. A better approach can be found in Spring Session's HttpSessionIdResolver interface, allowing developers to extract the core session ID directly from HTTP headers:

@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
  return HeaderHttpSessionIdResolver.xAuthToken(); // reads X-Auth-Token
}

With this configuration, the external gateway can handle initial authentication, generate a token-style session ID, and pass that token to internal microservices via a dedicated header. Every network hop securely accesses the exact same shared session. Because the local state is entirely eliminated, any instance of any service is perfectly equipped to handle any request.

The Proof of Concept: Sessions That Scale and Survive Anything

To demonstrate the resilience of session management with Valkey/Redis and Spring Boot, you can try this proof of concept.

You will need to deploy two separate instances of an account-service positioned behind an Nginx load balancer, which must be configured for simple round-robin routing. A single, shared Valkey container serves as the master session backend. The application reuses the SessionConfig, the Maven dependencies, and the myplatform:sessions namespace logic outlined above.

The proof-of-concept demo relies on a basic REST controller that manages a virtual shopping cart. Every HTTP response reports which specific backend instance processed the request. This allows you to watch the load balancer arbitrarily bounce a user between nodes while the shopping cart remains perfectly intact:

@RestController
public class CartController {
  @Value("${INSTANCE_ID:unknown}")
  private String instanceId;

  @PostMapping("/cart/{item}")
  public Map add(@PathVariable("item") String item, HttpSession session) {
    @SuppressWarnings("unchecked")
    List cart = (List) session.getAttribute("cart");
    if (cart == null) cart = new ArrayList<>();
    cart.add(item);
    session.setAttribute("cart", cart);
    return Map.of("servedBy", instanceId, "sessionId", session.getId(), "cart", cart);
  }

  @GetMapping("/cart")
  public Map view(HttpSession session) {
    Object cart = session.getAttribute("cart");
    return Map.of("servedBy", instanceId, "cart", cart == null ? List.of() : cart);
  }
}

Configure redisson.yaml (placed in src/main/resources/ to resolve properly via the classpath) to target the backend database by its localized Docker Compose hostname:

# redisson.yaml
singleServerConfig:
  address: "redis://valkey:6379"

The Nginx configuration forces aggressive round-robin balancing, actively preventing session pinning:

# nginx.conf
events {}
http {
  upstream account_service {
    server app1:8080;
    server app2:8080;
  }
  server {
    listen 8080;
    location / { proxy_pass http://account_service; }
  }
}

A multi-stage Dockerfile cleanly packages and executes the compiled application:

# Dockerfile
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q clean package -DskipTests

FROM eclipse-temurin:21-jre
COPY --from=build /app/target/*.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

Finally, the infrastructure is wired together using Docker Compose — spinning up one database, two identical application nodes, and the gateway load balancer:

# docker-compose.yml
services:
  valkey:
    image: valkey/valkey:9
    ports: ["6379:6379"]
  app1:
    build: .
    environment: { INSTANCE_ID: app1 }
    depends_on: [valkey]
  app2:
    build: .
    environment: { INSTANCE_ID: app2 }
    depends_on: [valkey]
  nginx:
    image: nginx:alpine
    ports: ["8080:8080"]
    volumes: ["./nginx.conf:/etc/nginx/nginx.conf:ro"]
    depends_on: [app1, app2]

Executing docker compose up --build brings the cluster online. You can simulate a browser's cookie management via the command line using a standard cookie jar:

# Add two items, then read the cart back
curl -c jar -b jar -X POST localhost:8080/cart/apple
curl -c jar -b jar -X POST localhost:8080/cart/pear
curl -b jar localhost:8080/cart
# {"servedBy":"app2","cart":["apple","pear"]}

Because this local demo operates over unencrypted HTTP, ensure the Secure cookie flag discussed earlier is temporarily disabled. Otherwise, curl will refuse to transmit the token and force an empty session generation upon every request.

Notice that the servedBy metadata aggressively alternates between app1 and app2 as Nginx shuffles the traffic. Despite the traffic distribution, the shopping cart payload remains flawlessly synchronized — proving that the session is completely decoupled from the JVM.

Why Choose Redisson Over Other Java Clients?

Development and engineering team leaders may point out that Spring Boot ships with Lettuce as its default client and natively supports Jedis. So, why bring Redisson, a separate Java client, into the mix?

While it's true in theory that Lettuce, Jedis, and Redisson can all back Spring Session, each client is suited for different purposes. If your only goal is to arbitrarily store session data in Valkey or Redis, Lettuce is perfectly adequate and avoids the need to add an extra third-party dependency. However, Lettuce lacks advanced functionality beyond simply writing and retrieving data. Similarly, Jedis is an older client that lacks thread safety, requires meticulous connection-pool tuning, and is rarely recommended for modern enterprise applications.

Meanwhile, Redisson is the best choice when session management scales beyond basic needs, with support for the following:

  • Tomcat-native integration: Redisson is the only one of the three to provide a first-party Tomcat session manager. This allows legacy web archives (WARs) and non-Spring applications to externalize sessions without modifying a single line of application code. Furthermore, it intelligently writes individual session attributes rather than inefficiently re-serializing the entire session payload upon every minor change.
    Enterprise Performance: The Redisson PRO edition includes a highly optimized Spring Session near-cache mechanism. This feature allows Spring Session to read from local application memory, entirely skipping the network round-trip to the external store, thereby significantly reducing latency for read-heavy workloads. (The community edition reads straight from the store, which remains fine for standard workloads.)

  • Unified distributed primitives: Redisson operates as a comprehensive distributed-objects platform. If an application requires distributed locks, maps, or rate limiters — for instance, utilizing a lock to safely guard concurrent writes to a shared session — utilizing Redisson consolidates these operations under a single, robust client architecture.

  • Topology resilience: Redisson seamlessly handles complex infrastructure topologies, including cluster, Sentinel, and replicated setups, executing transparent reconnections during network partitions.

Scalable Session Management and Other Essential Tools for Java Teams

Session management becomes exponentially more complex once you move beyond a monolithic JVM and into distributed applications behind a load balancer. But as the premier Java client for Valkey and Redis, Redisson simplifies scalable session management for distributed apps. This is but one advantage of Redisson and its more advanced counterpart, Redisson PRO. To learn more about the essential tools they provide today's Java development teams, review the Redisson and Redisson PRO feature comparison.