Use when writing or reviewing Java code that uses io.valkey:valkey-glide.
../assets/java-config.java- Client connection config templates, TLS/SSL, authentication (password, username, AWS IAM), cluster, standalone, etc.java-anti-patterns.md- Anti-patterns to avoid in Java GLIDE development, including exception handling, Hash vs JSON performance, thread safety, and more.
- Use Valkey GLIDE clients (
valkey-glide), NOT Jedis or Lettuce clients. - Avoid catching general exceptions when handling GLIDE errors - use specific exception types.
- Use batching / pipelining when suitable to group operations for efficiency.
- Prefer async chaining over blocking calls for production applications.
- Add comments to clarify sync vs async when not obvious from syntax.
Maven:
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
</build>
<dependencies>
<dependency>
<groupId>io.valkey</groupId>
<artifactId>valkey-glide</artifactId>
<classifier>${os.detected.classifier}</classifier>
<version>[2.0.0,)</version>
</dependency>
</dependencies>Gradle:
plugins {
id 'com.google.osdetector' version '1.7.3'
}
dependencies {
implementation group: 'io.valkey', name: 'valkey-glide', version: '2.+', classifier: osdetector.classifier
}Key Points:
- Classifier is required (native binaries per platform)
- Use
os-maven-pluginorosdetectorfor platform detection - Supports: linux-x86_64, linux-aarch_64, osx-x86_64, osx-aarch_64, windows-x86_64
// Do not use these
import redis.clients.jedis.*;
import io.lettuce.core.*;import glide.api.GlideClient;
import glide.api.GlideClusterClient;
import glide.api.models.configuration.GlideClientConfiguration;
import glide.api.models.configuration.GlideClusterClientConfiguration;
import glide.api.models.configuration.NodeAddress;
import glide.api.models.Batch;
import glide.api.models.ClusterBatch;
import glide.api.models.exceptions.RequestException;
import glide.api.models.exceptions.TimeoutException;
import glide.api.models.exceptions.ConnectionException;Use cluster client when:
- Running multiple GLIDE nodes
- Using multiple Valkey clusters
Otherwise, use standalone client.
- Production applications
- High concurrency requirements
- Non-blocking I/O frameworks (Netty, etc.)
- Better thread utilization
- Simple scripts or demos
- Sequential processing requirements
- Simpler code for prototypes
Use async method chaining, and narrow exception checking via instanceof in exceptionally handlers.
GlideClientConfiguration config = GlideClientConfiguration.builder()
.address(NodeAddress.builder()
.host("localhost")
.port(6379)
.build())
.requestTimeout(10000) // Recommended: explicit timeout
.build();
// Async (non-blocking) - using CompletableFuture chaining
GlideClient.createClient(config).thenCompose(client -> {
return client.set("key", "value")
.thenCompose(ok -> client.get("key"))
.thenAccept(value -> System.out.println(value))
.exceptionally(e -> {
// Direct access to GLIDE exceptions, no unwrapping
if (e instanceof RequestException) {
System.err.println("Request error: " + e.getMessage());
}
return null;
})
.whenComplete((v, e) -> {
try {
client.close();
} catch (ExecutionException ex) {
System.err.println("Error closing: " + ex.getMessage());
}
});
}).join(); // Only block at the endGlideClientConfiguration config = GlideClientConfiguration.builder()
.address(NodeAddress.builder()
.host("localhost")
.port(6379)
.build())
.requestTimeout(10000)
.build();
// Blocking (sync) - using .get() on CompletableFuture
try (GlideClient client = GlideClient.createClient(config).get()) {
client.set("key", "value").get();
String value = client.get("key").get();
System.out.println(value);
}Key Points:
- Client creation returns
CompletableFuture<GlideClient> - Async: Use
.thenCompose(),.thenAccept(), etc. for chaining - Blocking: Call
.get()or.join()to block thread - Always set explicit
requestTimeout()(default may be too short) - Use try-with-resources for automatic cleanup in blocking mode
These catch an ExecutionException, branch on the enclosed narrower exception, perform any narrow-specific processing, and then rethrows it.
Async chaining:
client.get("key")
.exceptionally(e -> {
// Exception may be wrapped - unwrap to check actual cause
Throwable cause = (e instanceof CompletionException && e.getCause() != null)
? e.getCause() : e;
if (cause instanceof RequestException) {
// Handle and optionally rethrow
System.err.println("Request error: " + cause.getMessage());
throw new CompletionException((RequestException) cause); // Rethrow
}
return null; // Or return default value
});Blocking with .get():
try {
client.get("key").get();
} catch (ExecutionException e) {
// Must unwrap: actual exception is in getCause()
if (e.getCause() instanceof RequestException) {
RequestException re = (RequestException) e.getCause();
// Handle error
}
}Blocking with .join():
try {
client.get("key").join();
} catch (CompletionException e) {
// Must unwrap: actual exception is in getCause()
if (e.getCause() instanceof RequestException) {
RequestException re = (RequestException) e.getCause();
// Handle error
}
}Key Finding: Async exceptions may arrive wrapped in CompletionException - unwrap with getCause() before checking type. Rethrow by wrapping in new CompletionException to propagate up the chain.
Async (recommended):
Batch pipeline = new Batch(false); // Non-atomic (pipeline)
pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.get("key1");
client.exec(pipeline, true)
.thenAccept(results -> {
// results is Object[]
System.out.println(Arrays.toString(results));
});Blocking:
Batch transaction = new Batch(true); // Atomic (transaction)
transaction.set("counter", "0");
transaction.incr("counter");
transaction.get("counter");
Object[] results = client.exec(transaction, true).get();
// results: [OK, 1, 1]- Constructor:
new Batch(boolean isAtomic)- positional parameter, not named - Execution:
client.exec(batch, raiseOnError)- camelCase parameter - Returns:
CompletableFuture<Object[]>- need casting for specific types raiseOnError=true: Throws first error as exceptionraiseOnError=false: Returns errors in result array asRequestErrorinstances- See SKILL.md for retry strategy decision matrix
The binaryOutput flag on the batch controls whether results use String or GlideString:
binaryOutput |
hgetall result type | get result type | How to set |
|---|---|---|---|
false (default) |
Map<String, String> |
String |
new Batch(false) |
true |
Map<GlideString, GlideString> |
GlideString |
new Batch(false).withBinaryOutput() |
// Default — results are String-based, safe to cast
Batch batch = new Batch(false);
batch.hgetall("user:1");
batch.get("name");
Object[] results = client.exec(batch, true).get();
Map<String, String> fields = (Map<String, String>) results[0]; // Correct
String name = (String) results[1]; // Correct// Binary output — results are GlideString-based
Batch batch = new Batch(false).withBinaryOutput();
batch.hgetall(gs("user:1"));
batch.get(gs("name"));
Object[] results = client.exec(batch, true).get();
Map<GlideString, GlideString> fields = (Map<GlideString, GlideString>) results[0]; // Correct
GlideString name = (GlideString) results[1]; // CorrectThe output type is controlled by binaryOutput, not by the key type passed to batch commands. A batch without .withBinaryOutput() always returns String-based maps even if you pass GlideString keys.
Retry on server errors:
import glide.api.models.commands.batch.ClusterBatchOptions;
import glide.api.models.commands.batch.ClusterBatchRetryStrategy;
ClusterBatchOptions options = ClusterBatchOptions.builder()
.retryStrategy(ClusterBatchRetryStrategy.builder()
.retryServerError(true)
.retryConnectionError(false)
.build())
.build();
Object[] results = client.exec(batch, true, options).get();Retry on connection errors:
ClusterBatchOptions options = ClusterBatchOptions.builder()
.retryStrategy(ClusterBatchRetryStrategy.builder()
.retryServerError(false)
.retryConnectionError(true)
.build())
.build();
Object[] results = client.exec(batch, true, options).get();Retry on both:
ClusterBatchOptions options = ClusterBatchOptions.builder()
.retryStrategy(ClusterBatchRetryStrategy.builder()
.retryServerError(true)
.retryConnectionError(true)
.build())
.build();
Object[] results = client.exec(batch, true, options).get();No retries:
ClusterBatchOptions options = ClusterBatchOptions.builder()
.retryStrategy(ClusterBatchRetryStrategy.builder()
.retryServerError(false)
.retryConnectionError(false)
.build())
.build();
Object[] results = client.exec(batch, true, options).get();import glide.api.models.exceptions.RequestException; // Command errors (WRONGTYPE, etc.)
import glide.api.models.exceptions.TimeoutException; // Request timeout
import glide.api.models.exceptions.ConnectionException; // Connection issuesProblem: Using legacy Redis clients
Solution: Always use valkey-glide package
Problem: Build fails with missing native library
Solution: Use os-maven-plugin or osdetector for automatic platform detection
Problem: Using .get() or .join() blocks threads, kills concurrency
Solution: Use async chaining with .thenCompose(), .thenAccept(), etc.
Problem: Operations return CompletableFuture, not values
Solution: Call .get() or .join() when blocking is acceptable
Problem: Connection timeouts on first request
Solution: Set explicit requestTimeout() in configuration (e.g., 10000ms)
Problem: Trying to import from glide.api.models.commands.batch.Batch
Solution: Import from glide.api.models.Batch
Problem: Catching Exception instead of specific GLIDE exceptions
Solution: Catch RequestException, TimeoutException, ConnectionException
Problem: Catching ExecutionException but not checking getCause()
Solution: Use e.getCause() to get actual GLIDE exception, or prefer async
Problem: Converting vector bytes to String corrupts the data
Solution: Use GlideString.of(byte[]) for binary data like vectors
Problem: Accessing results[1] when count is 0 causes IndexOutOfBoundsException
Solution: Check results.length > 1 before accessing documents map
Problem: Atomic batch or multi-key command with keys in different slots
Solution: Use hash tags {tag} to ensure keys map to same slot, or use non-atomic batch
import glide.api.commands.servermodules.FT;
import glide.api.models.commands.FT.FTCreateOptions;
import glide.api.models.commands.FT.FTCreateOptions.FieldInfo;
import glide.api.models.commands.FT.FTCreateOptions.VectorFieldFlat;
import glide.api.models.commands.FT.FTCreateOptions.DistanceMetric;
import glide.api.models.commands.FT.FTSearchOptions;
import glide.api.models.GlideString;FieldInfo[] schema = new FieldInfo[] {
new FieldInfo("embedding",
VectorFieldFlat.builder(DistanceMetric.COSINE, 768).build())
};
FT.create(client, "my_idx", schema).get();// Use GlideString for binary vector data
Map<GlideString, GlideString> doc = Map.of(
GlideString.of("embedding"), GlideString.of(vectorBytes),
GlideString.of("text"), GlideString.of("content")
);
client.hset(GlideString.of("doc:1"), doc).get();SECURITY: The => token in FT.SEARCH syntax separates a filter from a KNN clause. If user-controlled input (e.g., a filter parameter) contains =>, an attacker can inject a KNN query that bypasses all filters and returns all documents. Reject => in any user-supplied filter or field name before interpolating into query strings:
if (userFilter != null && userFilter.contains("=>")) {
throw new IllegalArgumentException("Filter must not contain '=>'");
}String query = "*=>[KNN 5 @embedding $vector AS score]";
FTSearchOptions opts = FTSearchOptions.builder()
.params(Map.of(GlideString.of("vector"), GlideString.of(queryVectorBytes)))
.build();
Object[] results = FT.search(client, "my_idx", query, opts).get();
Long count = (Long) results[0];
if (results.length > 1) {
Map<GlideString, Map<GlideString, GlideString>> docs =
(Map<GlideString, Map<GlideString, GlideString>>) results[1];
}// Drop index
FT.dropindex(client, "my_idx").get();
// Get info
Map<String, Object> info = FT.info(client, "my_idx").get();
// List indexes
GlideString[] indexes = FT.list(client).get();private static byte[] floatArrayToBytes(float[] array) {
ByteBuffer buffer = ByteBuffer.allocate(array.length * 4)
.order(ByteOrder.LITTLE_ENDIAN);
for (float f : array) {
buffer.putFloat(f);
}
return buffer.array();
}Key Points:
- FT methods are static on
FTclass, not client methods - Use
GlideString.of()factory method for binary data - Binary vectors use
GlideString, NOTString- converting bytes to String corrupts data - Search returns
Object[]:[count, documents_map] - Documents map only present if count > 0 - check
results.length > 1 - Use
ByteOrder.LITTLE_ENDIANfor vector encoding
// Standalone
import glide.api.GlideClient;
import glide.api.models.configuration.GlideClientConfiguration;
import glide.api.models.Batch;
// Cluster
import glide.api.GlideClusterClient;
import glide.api.models.configuration.GlideClusterClientConfiguration;
import glide.api.models.ClusterBatch;In cluster mode, data is distributed across 16384 hash slots. Each key hashes to a specific slot, and slots are distributed across nodes.
Atomic batches require same slot:
// Success - hash tags ensure same slot
ClusterBatch batch = new ClusterBatch(true);
batch.set("{user}:1", "Alice");
batch.set("{user}:2", "Bob");
client.exec(batch, true).get();
// Fails - different slots
ClusterBatch batch = new ClusterBatch(true);
batch.set("key1", "value1"); // Slot A
batch.set("key2", "value2"); // Slot B
client.exec(batch, true).get(); // RequestException: CROSSSLOTNon-atomic batches span slots:
ClusterBatch pipeline = new ClusterBatch(false);
pipeline.set("key1", "value1"); // Slot A
pipeline.set("key2", "value2"); // Slot B
client.exec(pipeline, true).get(); // SuccessMulti-key operations:
// Same slot - OK
client.del(new String[]{"{user}:1", "{user}:2"}).get();
// Different slots - CROSSSLOT error
client.del(new String[]{"key1", "key2"}).get(); // Fails
// Use non-atomic batch for multi-slot delete
ClusterBatch cleanup = new ClusterBatch(false);
cleanup.del(new String[]{"{user}:1", "{user}:2"});
cleanup.del(new String[]{"key1"});
cleanup.del(new String[]{"key2"});
Object[] results = client.exec(cleanup, true).get(); // [2, 1, 1]Key Points:
- Use hash tags
{tag}to control slot assignment - Atomic operations require all keys in same slot
- Non-atomic batches automatically route to multiple nodes
- GLIDE splits pipelines per node and reassembles responses
Spring Boot:
@Bean(destroyMethod = "close")
public GlideClient glideClient() throws ExecutionException, InterruptedException {
return GlideClient.createClient(config).get();
}Plain Java (shutdown hook):
GlideClient client = GlideClient.createClient(config).get();
Runtime.getRuntime().addShutdownHook(new Thread(client::close));Config templates: ../assets/java-config.java
import glide.api.GlideClusterClient;
import glide.api.models.configuration.GlideClusterClientConfiguration;
import glide.api.models.configuration.ReadFrom;
GlideClusterClientConfiguration config = GlideClusterClientConfiguration.builder()
.address(NodeAddress.builder()
.host("cluster.endpoint.cache.amazonaws.com")
.port(6379)
.build())
.readFrom(ReadFrom.AZ_AFFINITY)
.clientAZ("us-east-1a")
.requestTimeout(500)
.build();
GlideClusterClient client = GlideClusterClient.createClient(config).get();GlideClientConfiguration config = GlideClientConfiguration.builder()
.address(NodeAddress.builder().host("localhost").port(6379).build())
.inflightRequestsLimit(2000) // Default: 1000
.requestTimeout(500)
.build();GlideClientConfiguration config = GlideClientConfiguration.builder()
.address(NodeAddress.builder().host("localhost").port(6379).build())
.lazyConnect(true) // Defer connection until first command
.requestTimeout(500)
.build();import glide.api.models.configuration.BackoffStrategy;
GlideClientConfiguration config = GlideClientConfiguration.builder()
.address(NodeAddress.builder().host("localhost").port(6379).build())
.reconnectStrategy(BackoffStrategy.builder()
.numberOfRetries(10)
.factor(500)
.exponentBase(2)
.build())
.requestTimeout(500)
.build();GlideClient blockingClient = GlideClient.createClient(
GlideClientConfiguration.builder()
.address(NodeAddress.builder().host("localhost").port(6379).build())
.requestTimeout(30000)
.clientName("queue-worker")
.build()
).get();
String[] item = blockingClient.blpop(new String[]{"queue"}, 30).get();CompletableFuture<String> userFuture = client.get("user:123");
CompletableFuture<String[]> postsFuture = client.lrange("posts:123", 0, -1);
CompletableFuture.allOf(userFuture, postsFuture).join();
String user = userFuture.get();// Client shared across threads
private static final GlideClient client = createClient();
// Batch created per thread (because Batch objects are NOT thread-safe)
Batch batch = new Batch(false);
batch.get("key1");
client.exec(batch, true).get();import glide.api.OpenTelemetry;
OpenTelemetry.init(
OpenTelemetry.OpenTelemetryConfig.builder()
.traces(OpenTelemetry.TracesConfig.builder()
.endpoint("http://localhost:4318/v1/traces")
.samplePercentage(1)
.build())
.metrics(OpenTelemetry.MetricsConfig.builder()
.endpoint("http://localhost:4318/v1/metrics")
.build())
.build()
);import glide.api.logging.Logger;
Logger.setLoggerConfig(Logger.Level.WARN, "glide.log"); // Production
Logger.setLoggerConfig(Logger.Level.ERROR); // Max performanceServer-side config: server-configuration-guide.md