{
  "id": "java-lettuce",
  "title": "Redis session store with Java (Lettuce)",
  "url": "https://redis.io/docs/latest/develop/use-cases/session-store/java-lettuce/",
  "summary": "Implement a Redis-backed session store in Java with Lettuce",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc"
  ],
  "last_updated": "2026-04-20T16:36:49+01:00",
  "children": [],
  "page_type": "content",
  "content_hash": "d144d241d1571f6adf0434dde4b9aac2832363baaf6d6e96298ac62b890c04df",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "This guide shows you how to implement a Redis-backed session store in Java with [`Lettuce`](https://redis.io/docs/latest/develop/clients/lettuce). It includes both asynchronous and reactive store APIs, plus a small local demo server built on Java's built-in `HttpServer`."
    },
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Session storage is a common Redis use case for web applications. Instead of keeping session state in local process memory, you store it in Redis and send the browser only an opaque session ID in a cookie.\n\nThat gives you:\n\n* Shared sessions across multiple app servers\n* Automatic expiration using Redis TTLs\n* Fast reads and updates for small pieces of per-user state\n* A clean separation between browser cookies and server-side session data\n\nIn this example, each session is stored as a Redis hash with a key like `session:{session_id}`. The hash holds lightweight fields such as the username, page view count, timestamps, and the configured session TTL. The key also has an expiration so inactive sessions are removed automatically."
    },
    {
      "id": "why-async-and-reactive",
      "title": "Why async and reactive",
      "role": "content",
      "text": "For Lettuce, we generally show asynchronous and reactive APIs rather than a synchronous API:\n\n* Async with `RedisAsyncCommands` works well for standard Java applications using `CompletableFuture`\n* Reactive with `RedisReactiveCommands` is a good fit when you are already using Reactor\n* For synchronous Java session-store examples, we recommend [Jedis](https://redis.io/docs/latest/develop/use-cases/session-store/java-jedis)"
    },
    {
      "id": "how-it-works",
      "title": "How it works",
      "role": "content",
      "text": "The flow looks like this:\n\n1. A user submits a login form\n2. The server generates a random session ID with `SecureRandom`\n3. The server stores session data in Redis under `session:{id}`\n4. The server sends a `sid` cookie containing only the session ID\n5. Later requests read the cookie, load the hash from Redis, and refresh the TTL\n6. Logging out deletes the Redis key and clears the cookie\n\nBecause the cookie only contains an opaque identifier, the browser never receives the actual session data. That stays in Redis."
    },
    {
      "id": "the-lettuce-session-stores",
      "title": "The Lettuce session stores",
      "role": "content",
      "text": "The async and reactive session store classes wrap the basic session operations:\n\n* Async API: [AsyncSessionStore.java](AsyncSessionStore.java)\n* Reactive API: [ReactiveSessionStore.java](ReactiveSessionStore.java)"
    },
    {
      "id": "async-usage",
      "title": "Async usage",
      "role": "content",
      "text": "[code example]"
    },
    {
      "id": "reactive-usage",
      "title": "Reactive usage",
      "role": "content",
      "text": "[code example]"
    },
    {
      "id": "data-model",
      "title": "Data model",
      "role": "content",
      "text": "Each session is stored in a Redis hash:\n\n[code example]\n\nThe implementation uses:\n\n* [`HSET`](https://redis.io/docs/latest/commands/hset) to create and update session fields\n* [`HGETALL`](https://redis.io/docs/latest/commands/hgetall) to load the session\n* [`HINCRBY`](https://redis.io/docs/latest/commands/hincrby) to update counters\n* [`EXPIRE`](https://redis.io/docs/latest/commands/expire) to implement sliding expiration\n* [`DEL`](https://redis.io/docs/latest/commands/del) to remove a session on logout\n\nThe store treats `created_at`, `last_accessed_at`, and `session_ttl` as reserved internal fields, so caller-provided session data cannot overwrite them."
    },
    {
      "id": "session-store-implementation",
      "title": "Session store implementation",
      "role": "content",
      "text": "The `createSession()` method generates a random session ID, writes the initial hash fields, and sets the TTL:\n\n[code example]\n\nWhen the application reads a session, it refreshes the configured TTL so active users stay logged in:\n\n[code example]\n\nThis is a simple and effective pattern for many apps. For more complex requirements, you might add separate metadata keys, rotate session IDs after login, or store less frequently accessed data elsewhere."
    },
    {
      "id": "installation",
      "title": "Installation",
      "role": "setup",
      "text": "Add Lettuce to your project:\n\n* If you use **Maven**:\n\n  [code example]\n\n* If you use **Gradle**:\n\n  [code example]"
    },
    {
      "id": "running-the-demo",
      "title": "Running the demo",
      "role": "content",
      "text": "A local demo server is included to show the session store in action\n([source](DemoServer.java)):\n\n[code example]\n\nThe demo uses the async Lettuce API and exposes a small interactive page where you can:\n\n* Start a session with a username\n* Choose a short TTL and watch the session expire\n* See the Redis-backed session data rendered in the browser\n* Increment a page-view counter stored in Redis\n* Change the active session TTL from the page\n* Log out and delete the session\n\nThe demo assumes Redis is running on `localhost:6379`, but you can override that with `--redis-host` and `--redis-port`. After starting the server, visit `http://localhost:8080`."
    },
    {
      "id": "cookie-handling",
      "title": "Cookie handling",
      "role": "content",
      "text": "The browser cookie should contain only the session ID:\n\n[code example]\n\nAvoid storing user profiles, roles, or other sensitive session data directly in cookies. Keep that information in Redis and let the cookie act only as a lookup token."
    },
    {
      "id": "production-usage",
      "title": "Production usage",
      "role": "content",
      "text": "This guide uses a deliberately small local demo so you can focus on the Redis session pattern. In production, you will usually want to harden the cookie, session lifecycle, and deployment details around it."
    },
    {
      "id": "secure-the-session-cookie",
      "title": "Secure the session cookie",
      "role": "content",
      "text": "Set cookie attributes that match your deployment and threat model:\n\n* Keep `HttpOnly` enabled so JavaScript cannot read the session cookie\n* Use the `Secure` attribute when serving your app over HTTPS\n* Choose an appropriate `SameSite` policy for your login flow and cross-site behavior"
    },
    {
      "id": "keep-session-data-lightweight",
      "title": "Keep session data lightweight",
      "role": "content",
      "text": "Redis-backed sessions work best when each session stores small, frequently accessed values:\n\n* Usernames, IDs, and feature flags are a good fit\n* Large profiles, document blobs, or activity feeds should usually live elsewhere\n* Consider storing only references if the session needs to point to larger data"
    },
    {
      "id": "handle-expiration-deliberately",
      "title": "Handle expiration deliberately",
      "role": "content",
      "text": "Sliding expiration is convenient, but it also defines how long a hijacked cookie remains useful. For production apps, consider:\n\n* Shorter inactivity TTLs for sensitive applications\n* Separate absolute expiration limits for long-lived sessions\n* Session ID rotation after login or privilege changes"
    },
    {
      "id": "use-a-framework-integration-where-appropriate",
      "title": "Use a framework integration where appropriate",
      "role": "content",
      "text": "This example keeps everything explicit so you can see the Redis session pattern clearly. In a real app, you will often wrap the same Redis operations behind middleware for Spring WebFlux, Vert.x, Micronaut, or another Java framework using non-blocking I/O."
    },
    {
      "id": "next-steps",
      "title": "Next steps",
      "role": "content",
      "text": "You now have Redis-backed session examples in Java using both Jedis and Lettuce. From here you can:\n\n* Choose Jedis for synchronous apps or Lettuce for async/reactive apps\n* Add session ID rotation or absolute expiration\n* Store additional lightweight session metadata in the same Redis hash\n* Reuse the same Redis deployment across multiple application instances\n\nFor more Redis data modeling patterns, see:\n\n* [Session store overview](https://redis.io/docs/latest/develop/use-cases/session-store)\n* [Lettuce guide](https://redis.io/docs/latest/develop/clients/lettuce)\n* [Jedis session store](https://redis.io/docs/latest/develop/use-cases/session-store/java-jedis)"
    }
  ],
  "examples": [
    {
      "id": "async-usage-ex0",
      "language": "java",
      "code": "import io.lettuce.core.RedisClient;\nimport io.lettuce.core.api.StatefulRedisConnection;\nimport java.util.Map;\n\nRedisClient redisClient = RedisClient.create(\"redis://localhost:6379\");\nStatefulRedisConnection<String, String> connection = redisClient.connect();\n\nAsyncSessionStore store = new AsyncSessionStore(connection.async(), \"session:\", 1800);\n\nstore.createSession(Map.of(\n        \"username\", \"andrew\",\n        \"page_views\", \"0\"\n), null)\n    .thenCompose(sessionId -> store.getSession(sessionId, true))\n    .thenAccept(session -> {\n        if (session != null) {\n            System.out.println(session.get(\"username\"));\n        }\n    })\n    .join();\n\nconnection.close();\nredisClient.shutdown();",
      "section_id": "async-usage"
    },
    {
      "id": "reactive-usage-ex0",
      "language": "java",
      "code": "import io.lettuce.core.RedisClient;\nimport io.lettuce.core.api.StatefulRedisConnection;\nimport java.util.Map;\n\nRedisClient redisClient = RedisClient.create(\"redis://localhost:6379\");\nStatefulRedisConnection<String, String> connection = redisClient.connect();\n\nReactiveSessionStore store = new ReactiveSessionStore(connection.reactive(), \"session:\", 1800);\n\nstore.createSession(Map.of(\n        \"username\", \"andrew\",\n        \"page_views\", \"0\"\n), null)\n    .flatMap(sessionId -> store.getSession(sessionId, true))\n    .doOnNext(session -> System.out.println(session.get(\"username\")))\n    .block();\n\nconnection.close();\nredisClient.shutdown();",
      "section_id": "reactive-usage"
    },
    {
      "id": "data-model-ex0",
      "language": "text",
      "code": "session:abc123...\n  username = andrew\n  page_views = 3\n  session_ttl = 15\n  created_at = 2026-04-08T12:34:56Z\n  last_accessed_at = 2026-04-08T12:40:10Z",
      "section_id": "data-model"
    },
    {
      "id": "session-store-implementation-ex0",
      "language": "java",
      "code": "public CompletableFuture<String> createSession(Map<String, String> data, Integer ttl) {\n    String sessionId = createSessionId();\n    String key = sessionKey(sessionId);\n    String now = timestamp();\n    int sessionTtl = normalizeTtl(ttl);\n\n    Map<String, String> payload = sessionPayload(data, now, sessionTtl);\n\n    return commands.hset(key, payload)\n            .toCompletableFuture()\n            .thenCompose(ignore -> commands.expire(key, sessionTtl).toCompletableFuture())\n            .thenApply(ignore -> sessionId);\n}",
      "section_id": "session-store-implementation"
    },
    {
      "id": "session-store-implementation-ex1",
      "language": "java",
      "code": "public CompletableFuture<Map<String, String>> getSession(String sessionId, boolean refreshTtl) {\n    String key = sessionKey(sessionId);\n\n    return commands.hgetall(key).toCompletableFuture().thenCompose(session -> {\n        if (!isValidSession(session)) {\n            return CompletableFuture.completedFuture(null);\n        }\n        if (!refreshTtl) {\n            return CompletableFuture.completedFuture(session);\n        }\n\n        int sessionTtl = normalizeTtl(Integer.parseInt(session.get(\"session_ttl\")));\n        return commands.hset(key, \"last_accessed_at\", timestamp()).toCompletableFuture()\n                .thenCompose(ignore -> commands.expire(key, sessionTtl).toCompletableFuture())\n                .thenCompose(ignore -> commands.hgetall(key).toCompletableFuture())\n                .thenApply(refreshed -> isValidSession(refreshed) ? refreshed : null);\n    });\n}",
      "section_id": "session-store-implementation"
    },
    {
      "id": "installation-ex0",
      "language": "xml",
      "code": "<dependency>\n      <groupId>io.lettuce</groupId>\n      <artifactId>lettuce-core</artifactId>\n      <version>6.7.1.RELEASE</version>\n  </dependency>",
      "section_id": "installation"
    },
    {
      "id": "installation-ex1",
      "language": "groovy",
      "code": "implementation 'io.lettuce:lettuce-core:6.7.1.RELEASE'",
      "section_id": "installation"
    },
    {
      "id": "running-the-demo-ex0",
      "language": "bash",
      "code": "javac -cp lettuce-core-6.7.1.RELEASE.jar AsyncSessionStore.java DemoServer.java\njava -cp .:lettuce-core-6.7.1.RELEASE.jar DemoServer",
      "section_id": "running-the-demo"
    },
    {
      "id": "cookie-handling-ex0",
      "language": "java",
      "code": "exchange.getResponseHeaders().add(\n        \"Set-Cookie\",\n        \"sid=\" + sessionId + \"; Path=/; HttpOnly; SameSite=Lax\"\n);",
      "section_id": "cookie-handling"
    }
  ]
}
