Build CoreOrdered learning track

Java Client Landscape

Learn Java Redis In Action - Part 009

Java Redis client landscape for production systems: Jedis, Lettuce, Spring Data Redis, Redisson, selection criteria, abstraction boundaries, topology awareness, pooling, serialization, observability, and migration strategy.

22 min read4330 words
PrevNext
Lesson 0934 lesson track0718 Build Core
#java#redis#jedis#lettuce+4 more

Part 009 — Java Client Landscape: Jedis, Lettuce, Spring Data Redis, Redisson

Parts 001–008 built the Redis mental model and the core Redis data structure vocabulary. This part moves into the Java integration layer.

The most common Redis production mistake in Java systems is not choosing the "wrong" client. It is choosing a client without understanding what the client is hiding.

A Redis client is not just a dependency. It defines:

  • how commands are executed;
  • how connections are shared;
  • how timeouts are enforced;
  • how reconnect behaves during failure;
  • how cluster topology is discovered;
  • how values are serialized;
  • how much Redis behavior leaks into application code;
  • how much operational visibility you retain.

The goal of this part is to make client selection a deliberate engineering decision.


1. Kaufman Skill Decomposition

Target skill:

Given a Java service requirement, choose the Redis client abstraction that fits the workload, consistency boundary, topology, team capability, and operational risk — then justify the choice clearly.

Sub-skills:

Sub-skillWhat you must be able to do
Execution modelDistinguish synchronous, asynchronous, and reactive Redis usage
Connection modelUnderstand shared connections, pools, dedicated connections, and blocking-command boundaries
Abstraction modelKnow when low-level commands are safer than high-level convenience APIs
Serialization modelChoose value encoding intentionally and avoid Java native serialization as a default
Topology modelKnow how the client handles standalone, Sentinel, Cluster, TLS, and managed Redis
Failure modelPredict behavior during timeout, reconnect, failover, partial write, and command retry
Performance modelUse batching/pipelining without overloading Redis or hiding latency problems
Observability modelExpose metrics, command latency, error classes, pool state, and command mix
Migration modelMove between clients or abstractions without rewriting business logic everywhere

A top engineer does not ask only:

Which Java Redis client is fastest?

They ask:

Which client makes the failure modes of my workload explicit enough that I can operate it safely?

2. The Java Redis Client Stack

A Java service usually accesses Redis through one of four layers:

Think in layers:

LayerResponsibility
Business use caseSession lookup, quota check, idempotency, leaderboard update, stream consumption
Application Redis portYour own interface that expresses business intent
Client abstractionLettuce, Jedis, Spring Data Redis, Redisson
Protocol executionRESP command execution, pipelining, reconnect, topology handling
Redis topologyStandalone, Sentinel, Cluster, Redis Cloud, ElastiCache, Memorystore, Azure Cache

The important architectural move is the application Redis port.

Do not scatter client-specific calls across business code. For example, avoid this:

@Service
public class CheckoutService {
    private final RedisTemplate<String, Object> redisTemplate;

    public void checkout(String orderId) {
        redisTemplate.opsForValue().set("checkout:" + orderId, "processing");
        // business logic mixed with Redis implementation details
    }
}

Prefer this:

public interface CheckoutAttemptStore {
    boolean registerAttempt(String orderId, Duration ttl);
    void markCompleted(String orderId, Duration ttl);
}

Then implement the port with Lettuce, Jedis, or Spring Data Redis. This keeps Redis usage deliberate, testable, and replaceable.


3. The Four Main Options

3.1 Jedis

Jedis is a straightforward Java client with a synchronous programming model. It maps closely to Redis commands and is easy to reason about in imperative services.

Good fit:

  • synchronous Java services;
  • simple Redis command usage;
  • teams that prefer explicit pooling;
  • low abstraction overhead;
  • migration from older Redis Java codebases;
  • small-to-medium throughput workloads where blocking threads are acceptable.

Weak fit:

  • reactive stacks;
  • very high concurrency with limited platform threads;
  • workloads that need async command composition;
  • systems where connection usage is not carefully controlled.

Mental model:

application thread -> Jedis call -> socket write/read -> application thread waits

This is simple. That simplicity is often valuable. But simple does not mean free. Every Redis call occupies a Java thread until the command returns or times out.

3.2 Lettuce

Lettuce is a Netty-based Redis client supporting synchronous, asynchronous, and reactive APIs. It is often the default underlying driver for Spring Data Redis in modern Spring Boot applications.

Good fit:

  • services needing async Redis calls;
  • reactive applications;
  • high-concurrency workloads;
  • Redis Cluster/Sentinel usage;
  • shared connections for non-blocking commands;
  • topology-aware managed Redis deployments;
  • richer client-side configuration.

Weak fit:

  • teams unfamiliar with async/reactive failure modes;
  • codebases that accidentally block Netty/event-loop threads;
  • workloads using blocking Redis commands on shared connections;
  • business code that treats async retries as harmless.

Mental model:

application thread -> Lettuce command dispatch -> Netty event loop -> Redis -> future/reactive signal

Lettuce can expose sync commands too, but sync Lettuce is still built on the asynchronous engine. That means configuration and failure behavior are still governed by the underlying async client model.

3.3 Spring Data Redis

Spring Data Redis is an abstraction layer over Redis access. It provides RedisTemplate, StringRedisTemplate, repository support, cache abstraction integration, serializers, transactions integration, pub/sub support, and reactive Redis access.

Good fit:

  • Spring applications;
  • cache abstraction with @Cacheable, @CachePut, @CacheEvict;
  • teams already using Spring Boot configuration conventions;
  • services needing consistent serialization and bean-level configuration;
  • simple key-value/hash/list/set/zset operations without direct protocol plumbing.

Weak fit:

  • Redis-heavy systems where command-level behavior must be explicit;
  • advanced Lua/function workflows where abstraction adds friction;
  • cases where default serialization or cache naming is not controlled;
  • teams assuming annotations automatically solve consistency.

Mental model:

business code -> Spring abstraction -> serializer -> RedisConnectionFactory -> client driver -> Redis

Spring Data Redis is convenient, but convenience increases the distance between business code and Redis command semantics. That distance must be managed.

3.4 Redisson

Redisson provides high-level distributed Java objects and services backed by Redis: maps, locks, semaphores, queues, topics, executor-like constructs, and more.

Good fit:

  • teams needing high-level Redis-backed primitives;
  • distributed lock/lease convenience where correctness requirements are understood;
  • Java object-style APIs;
  • replacing local concurrent data structures with distributed variants in limited cases.

Weak fit:

  • teams that treat distributed objects like local objects;
  • correctness-critical coordination without fencing tokens or external validation;
  • systems where hidden Redis commands make performance/debugging difficult;
  • use cases where Redis data model should stay explicit.

Mental model:

Java object facade -> Redisson protocol behavior -> Redis data structures/scripts/pubsub

Redisson can be valuable, but it is dangerous when it makes distributed systems feel local. Remote state is not local memory. A network round trip is not a method call. A Redis lock is not a JVM monitor.


4. Comparison Matrix

DimensionJedisLettuceSpring Data RedisRedisson
Primary styleSynchronousSync, async, reactiveSpring abstractionHigh-level distributed objects
Learning curveLowMediumMediumMedium to high
Command visibilityHighHighMediumLow to medium
Async supportLimited / not primaryStrongVia reactive supportSupported internally / API-specific
Reactive supportNo primary modelYesYesNot the main reason to choose it
Spring Boot fitGoodExcellentExcellentGood
Cluster supportYesStrongDepends on driverYes
Sentinel supportYesStrongDepends on driverYes
Serialization controlManualCodec-based/manualSerializer-basedCodec-based/object abstraction
Best forSimple imperative RedisHigh-concurrency RedisSpring cache/template integrationDistributed object convenience
Biggest riskThread/pool exhaustionAsync misuse, reconnect assumptionsHidden serialization/cache semanticsTreating remote objects as local

This table is not a ranking. It is a risk map.


5. Selection Heuristics

5.1 Use Jedis when simplicity is the dominant constraint

Choose Jedis when:

  • your service is imperative;
  • Redis calls are short and bounded;
  • concurrency is moderate;
  • you want command-level control;
  • a pool-based mental model is acceptable;
  • the team values straightforward debugging over async flexibility.

Example use cases:

  • idempotency key store;
  • small cache-aside repository;
  • quota counter;
  • feature flag lookup;
  • simple sorted-set leaderboard update;
  • maintenance/admin utility.

A Jedis-based repository can be excellent when it is narrow and explicit.

public final class RedisIdempotencyStore implements IdempotencyStore {
    private final JedisPooled jedis;

    public RedisIdempotencyStore(JedisPooled jedis) {
        this.jedis = jedis;
    }

    @Override
    public boolean reserve(String key, Duration ttl) {
        String result = jedis.set(
            key,
            "reserved",
            new SetParams().nx().px(ttl.toMillis())
        );
        return "OK".equals(result);
    }
}

The code is simple because the workflow is simple. Do not add reactive complexity where there is no need.

5.2 Use Lettuce when concurrency, async, or topology matters

Choose Lettuce when:

  • you need async command composition;
  • you run a reactive stack;
  • you need robust Cluster/Sentinel integration;
  • connection sharing is helpful;
  • you want strong client configuration control;
  • Redis is a high-throughput component in the service.

Example use cases:

  • high-QPS read cache;
  • reactive gateway session lookup;
  • batch invalidation;
  • stream consumer pipeline;
  • cluster-aware multi-shard access;
  • latency-sensitive fanout.

Lettuce gives you more control, but more control means more responsibility.

5.3 Use Spring Data Redis when Spring integration is the dominant concern

Choose Spring Data Redis when:

  • you want Spring Boot configuration;
  • you need @Cacheable integration;
  • you need RedisTemplate or ReactiveRedisTemplate;
  • the team already standardizes on Spring data abstractions;
  • Redis is one infrastructure component among many.

Example use cases:

  • application-level cache;
  • token/session lookup;
  • simple Redis data access service;
  • cache manager with per-cache TTL;
  • Spring event listener invalidation.

But never accept the defaults blindly. The two default-risk areas are:

  1. serialization;
  2. cache key naming.

5.4 Use Redisson when high-level primitives are worth the hidden complexity

Choose Redisson when:

  • the team needs distributed object APIs;
  • you understand the Redis commands and scripts underneath;
  • operational visibility is still acceptable;
  • the primitive is not correctness-critical beyond Redis guarantees;
  • locks/leases are paired with fencing or validation where required.

Example use cases:

  • non-critical distributed semaphore;
  • duplicate worker suppression;
  • distributed map with clear TTL boundary;
  • simple leader-like coordination where correctness loss is tolerable.

Avoid Redisson when it is chosen because the team wants distributed systems to feel like local Java collections. That is exactly when it becomes dangerous.


6. Client Selection Decision Tree

The decision tree deliberately ends in "behind application port". The business layer should not care whether the implementation is Jedis, Lettuce, or Spring Data Redis.


7. The Client Abstraction Boundary

A good Redis abstraction is not generic. Generic Redis wrappers leak Redis everywhere.

Bad abstraction:

public interface RedisService {
    String get(String key);
    void set(String key, String value, Duration ttl);
    Long increment(String key);
    void delete(String key);
}

This looks clean but still exposes Redis as a generic key-value bag. It does not capture business invariants.

Better abstraction:

public interface LoginRateLimitStore {
    RateLimitDecision registerFailedAttempt(
        TenantId tenantId,
        UserId userId,
        Instant now
    );
}

Why better?

  • It hides key naming.
  • It owns TTL policy.
  • It owns script/atomicity decisions.
  • It can expose domain-specific outcomes.
  • It can be tested with fake, embedded, or testcontainer-backed implementations.
  • It gives you one place to migrate client libraries.

Redis usage should be explicit, but not scattered.


8. Connection Model Differences

8.1 Synchronous blocking model

In synchronous clients, every command blocks a Java thread.

This is easy to reason about. The failure mode is also easy to reason about:

slow Redis -> blocked request threads -> pool exhaustion -> cascading latency

Mitigations:

  • small command timeout;
  • bounded thread pools;
  • bulkheading;
  • circuit breaker for non-critical use cases;
  • fallback policy;
  • caching policy that does not retry infinitely;
  • metrics for Redis latency and pool wait time.

8.2 Async model

In async clients, command submission does not block the caller until you wait on the future.

The common trap:

String value = asyncCommands.get(key).get(); // turns async into blocking

This may be acceptable at an adapter boundary, but it defeats async composition if used everywhere.

8.3 Reactive model

Reactive Redis access is useful when the entire request pipeline is reactive.

It is not useful when:

  • the rest of the service blocks;
  • JDBC calls block inside the same flow;
  • the team has no backpressure discipline;
  • Redis commands are immediately .block()ed.

Reactive is an execution contract, not a performance decoration.

8.4 Dedicated connection boundaries

Some workloads should not share general-purpose connections:

WorkloadWhy dedicated connection helps
Blocking commandsBLPOP, BRPOP, blocking stream reads can occupy the connection
Pub/SubSubscribed connection enters a special mode and is not a normal command connection
TransactionsMULTI/EXEC state is connection-scoped
Long-running Lua/functionsCan delay unrelated commands on the same connection
Bulk pipelinesCan create response backlog and head-of-line blocking
Administrative scansSCAN jobs can distort latency of request-path commands

A production-grade service usually has multiple Redis access paths, not one global connection for everything.


9. Serialization Strategy Is Part of Client Selection

Serialization is not a minor detail. It is the schema of your Redis data.

Bad default:

whatever the framework serializes by default

Better default:

explicit key type + explicit value codec + explicit versioned envelope

Example envelope:

{
  "schemaVersion": 2,
  "type": "customer-session",
  "createdAt": "2026-07-02T13:00:00Z",
  "expiresAt": "2026-07-02T14:00:00Z",
  "payload": {
    "customerId": "cus_123",
    "tenantId": "tenant_a",
    "riskLevel": "LOW"
  }
}

Trade-offs:

FormatProsConsGood for
Plain stringHuman-readable, simpleLimited structurecounters, flags, status
JSONDebuggable, language-neutralLarger payload, parse overheadsessions, profiles, workflow state
Binary JSON / CBOR / SmileSmaller than JSON, structuredLess inspectablehigh-volume structured objects
ProtobufCompact, schema-drivenRequires schema toolingcross-service contract values
Java native serializationEasy by default in some stacksUnsafe history, brittle, opaquegenerally avoid
Custom binaryFast/small when done wellHard to evolve/debugextreme hot paths only

Serialization decisions affect:

  • memory usage;
  • network bandwidth;
  • latency;
  • debugging;
  • cross-language compatibility;
  • schema evolution;
  • migration complexity;
  • incident response.

For most production teams, JSON with explicit versioning is a strong default until measurement proves otherwise.


10. Spring Data Redis: Default Convenience and Hidden Risk

Spring Data Redis can be excellent, but it deserves special caution because it is easy to start using Redis without really designing Redis usage.

10.1 RedisTemplate

RedisTemplate<K, V> centralizes Redis operations and serialization.

Typical configuration:

@Configuration
public class RedisConfig {

    @Bean
    RedisTemplate<String, SessionEnvelope> sessionRedisTemplate(
            RedisConnectionFactory connectionFactory,
            ObjectMapper objectMapper) {

        RedisTemplate<String, SessionEnvelope> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, SessionEnvelope.class));
        template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, Object.class));
        template.afterPropertiesSet();
        return template;
    }
}

The key point is not the exact serializer class. The key point is that the serializer is explicit.

10.2 Cache abstraction

Spring cache annotations are convenient:

@Cacheable(cacheNames = "product-by-id", key = "#productId")
public ProductView getProduct(String productId) {
    return productRepository.findProductView(productId);
}

But @Cacheable does not answer:

  • What is the TTL?
  • Are nulls cached?
  • How is the key prefixed?
  • How is the value serialized?
  • What happens on Redis timeout?
  • How is cache invalidated after write?
  • Can stale data violate business rules?
  • Is the cache local, remote, or layered?

For serious systems, cache annotation usage must be backed by a written cache policy.

Example per-cache policy:

CacheTTLNull cachingConsistency targetInvalidation
product-by-id10 minutesYes, 30 seconds onlyEventualProduct change event
tenant-config60 secondsNoSoft real-timeVersion token + TTL
user-risk5 secondsNoConservativeBypass on critical action
exchange-rate15 minutesNoTime-boundedScheduled refresh

Without such a policy, annotation-based caching becomes accidental distributed state.


11. Redisson: Distributed Objects Are Not Local Objects

Redisson can expose APIs that feel familiar to Java developers:

RMap<String, CustomerSession> sessions = redisson.getMap("sessions");
CustomerSession session = sessions.get(sessionId);

This reads well. But the mental model must be:

remote Redis command(s), serialization, network latency, topology, retry, failure, consistency limit

Not:

HashMap.get()

11.1 Lock example caution

A Redisson lock may look like this:

RLock lock = redisson.getLock("order-lock:" + orderId);
boolean acquired = lock.tryLock(100, 10, TimeUnit.SECONDS);

This hides a lot:

  • lock key creation;
  • lease timeout;
  • renewal/watchdog behavior;
  • unlock script;
  • client crash behavior;
  • Redis failover behavior;
  • duplicate owner possibility under extreme failure;
  • lack of fencing unless you add it.

For efficiency locks, this may be fine. For correctness locks, you need deeper design. Part 018 will cover this in detail.

11.2 Redisson fit rule

Use Redisson when:

The high-level primitive reduces implementation risk more than it hides operational risk.

Do not use it when:

The high-level primitive lets the team avoid understanding the distributed behavior.

12. Topology Awareness

Redis access differs across topologies.

TopologyClient responsibility
StandaloneConnect to one endpoint, handle reconnect
ReplicaDecide whether reads can go to replica and tolerate staleness
SentinelDiscover current primary, reconnect after failover
ClusterRoute commands by hash slot, follow redirects, handle topology refresh
Managed RedisRespect provider endpoints, TLS, IAM/auth model, failover behavior, maintenance windows

Client choice matters more when topology is not standalone.

12.1 Cluster-specific concerns

Redis Cluster routes keys by slot. That has consequences for Java clients:

  • client must understand MOVED/ASK redirects;
  • multi-key commands must target the same slot unless using client-side scatter/gather;
  • hash tags can force related keys into one slot;
  • topology refresh must detect resharding/failover;
  • pipeline behavior is more complex across shards.

A client that works fine in standalone mode can still be misused in cluster mode.

12.2 Sentinel-specific concerns

For Sentinel, the client must discover the current primary. After failover, stale connections must reconnect. The application must expect a short period of errors, timeouts, or retries.

Do not set retries so aggressively that all services stampede the new primary.


13. Timeout, Retry, and Reconnect Policy

Every Redis client usage must answer these questions:

QuestionWhy it matters
What is the command timeout?Prevents request threads/futures from hanging too long
What happens when the connection is down?Queue, fail fast, reconnect, or drop?
Are commands retried?Retrying non-idempotent commands can duplicate effects
Are writes idempotent?Determines whether retry is safe
Is Redis critical or optional for this request?Determines fallback behavior
Is the client allowed to buffer commands during outage?Prevents memory buildup and surprise replay

A dangerous policy:

Long timeout + unlimited retry + non-idempotent command + no circuit breaker

A safer policy:

Short timeout + bounded retry only for idempotent reads + fallback or fail-closed per use case

13.1 Retry classification

Command/workflowRetry safe?Notes
GET keyUsually yesUnless timing itself matters
SET key valueUsually yesDepends on overwrite semantics
SET key value NX PX ttlUsually yes-ishResponse ambiguity after timeout still matters
INCR keyNo by defaultRetry can double increment
LPUSH queue itemNo by defaultRetry can duplicate item
XADD stream * ...No by defaultRetry can duplicate event unless idempotency is external
Lua idempotency scriptUsually yesIf script itself encodes idempotency
DEL keyOften yesBut can delete newly-created value if key reused incorrectly

Timeout is not proof of failure. The command may have reached Redis and the response may have been lost. This is central to distributed systems.


14. Pooling and Resource Management

14.1 When pooling helps

Pooling helps when:

  • connections are not safe to share for the workload;
  • calls are synchronous and blocking;
  • transactions or blocking commands need isolated connections;
  • framework integration expects pooled resources;
  • you need bounded concurrency into Redis.

Pooling does not make Redis faster by itself. It mostly controls concurrency and avoids connection creation overhead.

14.2 Pool sizing mental model

For synchronous Redis usage:

needed connections ≈ concurrent Redis operations during peak

But do not size the pool only to eliminate wait time. A very large pool can overload Redis.

Better:

pool size = controlled concurrency budget

If the pool saturates, that is a signal. It means the application is trying to send more Redis work than its budget allows.

14.3 Metrics to expose

MetricWhy it matters
Active connectionsIndicates concurrency pressure
Idle connectionsIndicates sizing efficiency
Pool wait timeDetects app-side backpressure
Borrow timeout countEarly sign of saturation
Command timeout countRedis/network/server-side issue
Reconnect countNetwork/topology instability
Command latency histogramTrue user-facing impact
Error class countSeparates timeout, auth, moved, no-script, OOM, read-only

A Redis outage should be visible as a shaped failure, not as random application slowness.


15. Observability by Client Type

ClientWhat to monitor
Jedispool active/idle/wait, command latency, timeout/error count, connection creation
Lettucecommand latency, event loop health, reconnect count, pending command queue, topology refresh, timeout/error count
Spring Data Redisunderlying driver metrics, cache hits/misses, serialization errors, cache null count, per-cache latency
Redissonlock wait/hold time, watchdog/renewal failures, command latency, object operation count, retries

Do not monitor only Redis server. Client-side behavior can be the bottleneck.

Example incident:

Redis server CPU: 15%
Redis server latency: low
Application Redis timeout: high
Root cause: exhausted Jedis pool due to slow downstream thread holding pooled resources

Another example:

Redis server healthy
Application latency spikes
Root cause: large async command queue during reconnect, then burst replay after connection returns

Both incidents are client-side operational problems.


16. Dependency and Version Strategy

16.1 Avoid dependency drift

Redis clients are infrastructure dependencies. Treat them like database drivers.

Rules:

  • pin versions intentionally;
  • read release notes before major upgrades;
  • test against your Redis server version/topology;
  • run integration tests for failover/cluster redirects;
  • include serialization compatibility tests;
  • include Lua/function compatibility tests;
  • do not upgrade Spring Boot and Redis driver blindly together.

16.2 Maven examples

Jedis:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>${jedis.version}</version>
</dependency>

Lettuce:

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>${lettuce.version}</version>
</dependency>

Spring Data Redis through Spring Boot starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Redisson:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>${redisson.version}</version>
</dependency>

Do not copy versions from old blog posts. Use the versions aligned with your runtime, Spring Boot BOM, Redis server version, and security baseline.


17. Production Configuration Checklist

Regardless of client, define these explicitly:

ConfigurationRequired decision
Endpointstandalone, Sentinel, Cluster, managed endpoint
TLSenabled/disabled, certificate verification, trust store
Authenticationusername/password or provider-specific auth mechanism
Database indexavoid relying on logical DBs in Cluster; prefer namespace prefixes
Command timeoutper workload, not arbitrary global default
Connect timeoutfail fast enough for service startup/readiness
Retry policycommand-aware, bounded, idempotency-aware
Reconnect behaviorqueue vs fail fast during disconnect
Pool sizeconcurrency budget, not infinite capacity
Serializationexplicit, versioned, and tested
Key prefixservice, environment, tenant, domain boundary
Metricsclient + server + cache-level metrics
Loggingsanitized endpoint, error class, command family, not secrets
Health checkcommand-level readiness, not only socket connect

A Redis client should never be configured only by default values in a production system.


18. Anti-Patterns

18.1 One global generic Redis utility

@Component
public class RedisUtil {
    public Object get(String key) { ... }
    public void set(String key, Object value) { ... }
    public void del(String key) { ... }
}

Why bad:

  • no domain ownership;
  • no TTL policy;
  • no serialization boundary;
  • no observability by use case;
  • impossible to reason about consistency;
  • encourages Redis as a shared junk drawer.

18.2 Blind @Cacheable

@Cacheable("users")
public User getUser(String id) { ... }

What is missing:

  • TTL;
  • key prefix;
  • null policy;
  • tenant isolation;
  • invalidation;
  • stale data tolerance;
  • serialization contract;
  • fallback behavior.

18.3 Reactive wrapper over blocking usage

Mono.fromCallable(() -> jedis.get(key))

This is not true non-blocking Redis. It may be acceptable with bounded scheduler isolation, but it is not the same as Lettuce reactive.

18.4 Async command firehose

for (String key : keys) {
    async.get(key); // no bound, no backpressure, no tracking
}

This creates an unbounded client-side queue. Bound concurrency. Batch intentionally. Measure pending commands.

18.5 Distributed object illusion

RMap<String, Order> orders = redisson.getMap("orders");
orders.get(orderId).markPaid();

This can imply local mutation semantics that do not exist. Remote objects should be treated as serialized state transitions.


19. Migration Strategy Between Clients

You may start with Spring Data Redis and later need Lettuce directly. Or you may have old Jedis code and migrate to Lettuce. Or you may remove Redisson from correctness-critical paths.

A safe migration strategy:

Contract test example:

public interface IdempotencyStoreContract {

    IdempotencyStore store();

    @Test
    default void reserveReturnsTrueOnlyOnce() {
        String key = "test:idempotency:" + UUID.randomUUID();

        assertThat(store().reserve(key, Duration.ofMinutes(5))).isTrue();
        assertThat(store().reserve(key, Duration.ofMinutes(5))).isFalse();
    }
}

Every implementation must satisfy the same behavior. That is how you prevent client migration from changing business semantics.


20. Reference Architectures

20.1 Simple imperative service

Good when:

  • request volume is moderate;
  • each Redis operation is short;
  • the workflow is simple;
  • command behavior must be explicit.

20.2 Spring application cache

Good when:

  • method-level cache is appropriate;
  • stale data tolerance is documented;
  • per-cache TTL is configured;
  • serializer is explicit;
  • errors/fallbacks are understood.

20.3 High-concurrency async service

Good when:

  • high concurrency matters;
  • command composition is async;
  • pending command pressure is measured;
  • topology refresh is configured;
  • blocking operations use separate connections.

20.4 Redisson distributed primitive

Good when:

  • the primitive reduces implementation complexity;
  • the correctness boundary is externalized when needed;
  • lock duration and failure behavior are tested;
  • operational metrics exist.

21. Practical Exercises

Exercise 1 — Client selection memo

Write a one-page decision memo for each workload:

  1. user session cache;
  2. payment idempotency marker;
  3. search result cache;
  4. WebSocket presence map;
  5. delayed retry queue;
  6. distributed lock for nightly job;
  7. reactive API gateway rate limiter.

For each, answer:

  • client choice;
  • execution model;
  • topology assumption;
  • serialization format;
  • timeout policy;
  • retry policy;
  • fallback behavior;
  • metrics required.

Exercise 2 — Build application ports

Define Java interfaces for:

  • SessionStore;
  • RateLimitStore;
  • IdempotencyStore;
  • LeaderboardStore;
  • NotificationSignalBus.

The interface must not expose Redis terms unless Redis is the actual domain concept.

Exercise 3 — Failure table

For one Redis use case, create a table:

FailureExpected behaviorTest method
Redis timeout??
Redis unavailable??
Partial write ambiguity??
Failover??
Serialization error??
Hot key??

This exercise forces client selection to include operations, not just API preference.


22. Summary

Java Redis client choice is not about popularity. It is about matching client behavior to workload behavior.

Use this rule of thumb:

NeedLikely choice
Simple imperative command usageJedis or Lettuce sync
High concurrency / async / reactiveLettuce
Spring cache/template integrationSpring Data Redis, usually backed by Lettuce
High-level distributed objectsRedisson, behind narrow ports and with failure discipline
Maximum command transparencyJedis or direct Lettuce
Maximum Spring conventionSpring Data Redis
Correctness-critical coordinationDo not rely on client abstraction alone; design fencing/validation

The core engineering invariant:

Client libraries reduce protocol work. They do not remove distributed systems failure modes.

Part 010 will go deep into Lettuce because it is the most flexible Java Redis client and is commonly used directly or indirectly through Spring Data Redis.

Lesson Recap

You just completed lesson 09 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.