diff --git a/performance/README.md b/performance/README.md
new file mode 100644
index 00000000..a28fa85f
--- /dev/null
+++ b/performance/README.md
@@ -0,0 +1,79 @@
+# Spring Data Valkey Performance Tests
+
+Performance benchmarks for Spring Data Valkey operations across different clients.
+
+## Prerequisites
+
+- JDK 17 or higher
+- Maven 3.9.9 or higher (use `../mvnw` from root directory)
+- Valkey server running on `localhost:6379` (or configure connection in tests)
+
+If using a development build of Spring Data Valkey, first install to your local Maven repository before running the tests:
+```bash
+# From project root
+$ ./mvnw clean install -DskipTests
+```
+
+See instructions on starting a Valkey server using the `Makefile` in the root [README](../README.md#building-from-source). The standalone instance started by the Makefile is used in these tests.
+
+## Running Tests
+
+### Template Performance Test
+
+Test ValkeyTemplate operations (`SET`, `GET`, `DELETE`) with different clients:
+
+```bash
+$ mvn -q compile exec:java -Dclient=valkeyglide
+$ mvn -q compile exec:java -Dclient=lettuce
+$ mvn -q compile exec:java -Dclient=jedis
+```
+
+### Multi-Threaded Performance Test
+
+Test template use across mulitple threads with different clients:
+
+```bash
+$ mvn -q compile exec:java@threaded-test -Dclient=valkeyglide
+$ mvn -q compile exec:java@threaded-test -Dclient=lettuce
+$ mvn -q compile exec:java@threaded-test -Dclient=jedis
+```
+
+### Direct Client Performance Test
+
+Test direct client operations without Spring Data Valkey (for comparison):
+
+```bash
+$ mvn -q compile exec:java@direct-test -Dclient=valkeyglide
+$ mvn -q compile exec:java@direct-test -Dclient=lettuce
+$ mvn -q compile exec:java@direct-test -Dclient=jedis
+```
+
+### Multi-Threaded Direct Client Performance Test
+
+Test direct client operations across multiple threads:
+
+```bash
+$ mvn -q compile exec:java@threaded-direct-test -Dclient=valkeyglide
+$ mvn -q compile exec:java@threaded-direct-test -Dclient=lettuce
+$ mvn -q compile exec:java@threaded-direct-test -Dclient=jedis
+```
+
+### Template Load Test
+
+Test ValkeyTemplate operations (`SET`, `GET`, `DELETE`) with different clients and concurrency levels.
+
+Parameters:
+- `client`: Client type - `valkeyglide`, `lettuce`, `jedis` (default: `valkeyglide`)
+- `threads`: Number of threads (default: `10`)
+- `operations`: Operations per thread (default: `50`)
+
+```bash
+# Compare a single client with different concurrency levels
+$ mvn -q compile exec:java@load-test -Dclient=valkeyglide -Dthreads=5 -Doperations=20
+$ mvn -q compile exec:java@load-test -Dclient=valkeyglide -Dthreads=20 -Doperations=100
+
+# Comapre across different clients
+$ mvn -q compile exec:java@load-test -Dclient=valkeyglide -Dthreads=100 -Doperations=200
+$ mvn -q compile exec:java@load-test -Dclient=lettuce -Dthreads=100 -Doperations=200
+$ mvn -q compile exec:java@load-test -Dclient=jedis -Dthreads=100 -Doperations=200
+```
diff --git a/performance/pom.xml b/performance/pom.xml
new file mode 100644
index 00000000..cc5b9d56
--- /dev/null
+++ b/performance/pom.xml
@@ -0,0 +1,108 @@
+
+
+ 4.0.0
+
+ io.valkey.springframework.data
+ spring-data-valkey-performance
+ 1.0.0
+ pom
+
+ Spring Data Valkey - Performance Tests
+ Performance benchmarks for Spring Data Valkey operations across different clients
+
+
+ UTF-8
+ 17
+ 3.5.1
+ 2.1.1
+ 6.4.0.RELEASE
+ 5.2.0
+ 2.0.9
+
+
+
+
+ io.valkey.springframework.data
+ spring-data-valkey
+ ${spring-data-valkey.version}
+
+
+ io.valkey
+ valkey-glide
+ ${valkey-glide.version}
+ ${os.detected.classifier}
+
+
+ io.lettuce
+ lettuce-core
+ ${lettuce.version}
+
+
+ redis.clients
+ jedis
+ ${jedis.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.7.1
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${maven.compiler.release}
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ performance.TemplatePerformanceTest
+ false
+
+
+
+ threaded-test
+
+ performance.MultiThreadedPerformanceTest
+
+
+
+ direct-test
+
+ performance.DirectClientPerformanceTest
+
+
+
+ threaded-direct-test
+
+ performance.ThreadedDirectClientPerformanceTest
+
+
+
+ load-test
+
+ performance.TemplateLoadTest
+
+
+
+
+
+
+
diff --git a/performance/src/main/java/performance/DirectClientPerformanceTest.java b/performance/src/main/java/performance/DirectClientPerformanceTest.java
new file mode 100644
index 00000000..7bccdbde
--- /dev/null
+++ b/performance/src/main/java/performance/DirectClientPerformanceTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package performance;
+
+import glide.api.GlideClient;
+import glide.api.models.GlideString;
+import glide.api.models.configuration.GlideClientConfiguration;
+import glide.api.models.configuration.NodeAddress;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
+import io.lettuce.core.api.StatefulRedisConnection;
+import io.lettuce.core.api.sync.RedisCommands;
+import redis.clients.jedis.Jedis;
+
+/**
+ * Direct client performance test without Spring Data Valkey overhead.
+ */
+public class DirectClientPerformanceTest {
+
+ private static final int OPERATIONS = 10000;
+ private static final String KEY_PREFIX = "direct:test:";
+
+ public static void main(String[] args) throws Exception {
+ String clientType = System.getProperty("client", "valkeyglide");
+
+ System.out.println("Running Direct Client Performance Test");
+ System.out.println("Client: " + clientType);
+ System.out.println("Operations: " + OPERATIONS);
+ System.out.println("----------------------------------------");
+
+ switch (clientType.toLowerCase()) {
+ case "valkeyglide" -> testValkeyGlide();
+ case "lettuce" -> testLettuce();
+ case "jedis" -> testJedis();
+ default -> throw new IllegalArgumentException("Unknown client: " + clientType);
+ }
+ }
+
+ private static void testValkeyGlide() throws Exception {
+ GlideClientConfiguration config = GlideClientConfiguration.builder()
+ .address(NodeAddress.builder().host("localhost").port(6379).build())
+ .build();
+
+ try (GlideClient client = GlideClient.createClient(config).get()) {
+ // SET operations
+ long start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ client.set(GlideString.of(KEY_PREFIX + i), GlideString.of("value" + i)).get();
+ }
+ long setTime = System.nanoTime() - start;
+ printResult("SET", setTime);
+
+ // GET operations
+ start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ client.get(GlideString.of(KEY_PREFIX + i)).get();
+ }
+ long getTime = System.nanoTime() - start;
+ printResult("GET", getTime);
+
+ // DELETE operations
+ start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ client.del(new GlideString[]{GlideString.of(KEY_PREFIX + i)}).get();
+ }
+ long deleteTime = System.nanoTime() - start;
+ printResult("DELETE", deleteTime);
+ }
+ }
+
+ private static void testLettuce() {
+ RedisClient client = RedisClient.create(RedisURI.create("redis://localhost:6379"));
+
+ try (StatefulRedisConnection connection = client.connect()) {
+ RedisCommands commands = connection.sync();
+
+ // SET operations
+ long start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ commands.set(KEY_PREFIX + i, "value" + i);
+ }
+ long setTime = System.nanoTime() - start;
+ printResult("SET", setTime);
+
+ // GET operations
+ start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ commands.get(KEY_PREFIX + i);
+ }
+ long getTime = System.nanoTime() - start;
+ printResult("GET", getTime);
+
+ // DELETE operations
+ start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ commands.del(KEY_PREFIX + i);
+ }
+ long deleteTime = System.nanoTime() - start;
+ printResult("DELETE", deleteTime);
+ } finally {
+ client.shutdown();
+ }
+ }
+
+ private static void testJedis() {
+ try (Jedis jedis = new Jedis("localhost", 6379)) {
+ // SET operations
+ long start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ jedis.set(KEY_PREFIX + i, "value" + i);
+ }
+ long setTime = System.nanoTime() - start;
+ printResult("SET", setTime);
+
+ // GET operations
+ start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ jedis.get(KEY_PREFIX + i);
+ }
+ long getTime = System.nanoTime() - start;
+ printResult("GET", getTime);
+
+ // DELETE operations
+ start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ jedis.del(KEY_PREFIX + i);
+ }
+ long deleteTime = System.nanoTime() - start;
+ printResult("DELETE", deleteTime);
+ }
+ }
+
+ private static void printResult(String operation, long durationNanos) {
+ long durationMs = durationNanos / 1_000_000;
+ System.out.printf("%s: %,d ops/sec (%.2f ms total)%n",
+ operation, (long) (OPERATIONS * 1000.0 / durationMs), durationMs / 1.0);
+ }
+}
diff --git a/performance/src/main/java/performance/MultiThreadedPerformanceTest.java b/performance/src/main/java/performance/MultiThreadedPerformanceTest.java
new file mode 100644
index 00000000..98311538
--- /dev/null
+++ b/performance/src/main/java/performance/MultiThreadedPerformanceTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package performance;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.IntStream;
+
+import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory;
+import io.valkey.springframework.data.valkey.connection.jedis.JedisConnectionFactory;
+import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory;
+import io.valkey.springframework.data.valkey.connection.valkeyglide.ValkeyGlideConnectionFactory;
+import io.valkey.springframework.data.valkey.core.StringValkeyTemplate;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * Multi-threaded performance test for ValkeyTemplate operations across different clients.
+ */
+public class MultiThreadedPerformanceTest {
+
+ private static final int THREADS = 100;
+ private static final int OPERATIONS_PER_THREAD = 100;
+ private static final int TOTAL_OPERATIONS = THREADS * OPERATIONS_PER_THREAD;
+
+ public static void main(String[] args) throws Exception {
+ String clientType = System.getProperty("client", "valkeyglide");
+
+ System.out.println("Running Multi-Threaded Performance Test");
+ System.out.println("Client: " + clientType);
+ System.out.println("Threads: " + THREADS);
+ System.out.println("Operations per thread: " + OPERATIONS_PER_THREAD);
+ System.out.println("Total operations: " + TOTAL_OPERATIONS);
+ System.out.println("----------------------------------------");
+
+ ValkeyConnectionFactory factory = createConnectionFactory(clientType);
+ if (factory instanceof InitializingBean) {
+ ((InitializingBean) factory).afterPropertiesSet();
+ }
+
+ try {
+ runMultiThreadedTest(factory);
+ } finally {
+ if (factory instanceof DisposableBean) {
+ ((DisposableBean) factory).destroy();
+ }
+ }
+ }
+
+ private static ValkeyConnectionFactory createConnectionFactory(String clientType) {
+ return switch (clientType.toLowerCase()) {
+ case "lettuce" -> new LettuceConnectionFactory();
+ case "jedis" -> new JedisConnectionFactory();
+ case "valkeyglide" -> new ValkeyGlideConnectionFactory();
+ default -> throw new IllegalArgumentException("Unknown client: " + clientType);
+ };
+ }
+
+ private static void runMultiThreadedTest(ValkeyConnectionFactory factory) throws InterruptedException {
+ long startTime = System.currentTimeMillis();
+
+ ExecutorService executorService = Executors.newFixedThreadPool(THREADS);
+
+ StringValkeyTemplate template = new StringValkeyTemplate(factory);
+
+ AtomicInteger setOperations = new AtomicInteger(0);
+ AtomicInteger getOperations = new AtomicInteger(0);
+ AtomicInteger deleteOperations = new AtomicInteger(0);
+ AtomicInteger setFailures = new AtomicInteger(0);
+ AtomicInteger getFailures = new AtomicInteger(0);
+ AtomicInteger deleteFailures = new AtomicInteger(0);
+
+ try {
+ Runnable task = () -> {
+ try {
+ IntStream.range(0, OPERATIONS_PER_THREAD).forEach(i -> {
+ String key = Thread.currentThread().getName() + ":" + i;
+ String value = "value" + i;
+
+ // SET operation
+ try {
+ template.opsForValue().set(key, value);
+ setOperations.incrementAndGet();
+ } catch (Exception e) {
+ setFailures.incrementAndGet();
+ }
+
+ // GET operation
+ try {
+ String result = template.opsForValue().get(key);
+ if (result != null) {
+ getOperations.incrementAndGet();
+ }
+ } catch (Exception e) {
+ getFailures.incrementAndGet();
+ }
+
+ // DELETE operation
+ try {
+ template.delete(key);
+ deleteOperations.incrementAndGet();
+ } catch (Exception e) {
+ deleteFailures.incrementAndGet();
+ }
+ });
+ } catch (Exception e) {
+ System.err.println("Thread failed: " + e.getMessage());
+ setFailures.addAndGet(OPERATIONS_PER_THREAD);
+ getFailures.addAndGet(OPERATIONS_PER_THREAD);
+ deleteFailures.addAndGet(OPERATIONS_PER_THREAD);
+ }
+ };
+
+ IntStream.range(0, THREADS).forEach(i -> executorService.submit(task));
+
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long duration = System.currentTimeMillis() - startTime;
+
+ printResult("SET", duration, setOperations.get(), setFailures.get());
+ printResult("GET", duration, getOperations.get(), getFailures.get());
+ printResult("DELETE", duration, deleteOperations.get(), deleteFailures.get());
+ } finally {
+ executorService.shutdown();
+ }
+ }
+
+ private static void printResult(String operation, long duration, int successful, int failed) {
+ System.out.printf("%s: %,d ops/sec (%.2f ms total), %.1f%% successful%n",
+ operation, (long) (successful * 1000.0 / duration), duration / 1.0, (successful * 100.0 / TOTAL_OPERATIONS));
+ }
+}
diff --git a/performance/src/main/java/performance/TemplateLoadTest.java b/performance/src/main/java/performance/TemplateLoadTest.java
new file mode 100644
index 00000000..54c02f9f
--- /dev/null
+++ b/performance/src/main/java/performance/TemplateLoadTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package performance;
+
+import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory;
+import io.valkey.springframework.data.valkey.connection.jedis.JedisConnectionFactory;
+import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory;
+import io.valkey.springframework.data.valkey.connection.valkeyglide.ValkeyGlideConnectionFactory;
+import io.valkey.springframework.data.valkey.core.StringValkeyTemplate;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.IntStream;
+
+/**
+ * Parameterized load test for ValkeyTemplate using different clients and under various concurrency levels.
+ */
+public class TemplateLoadTest {
+
+ public static void main(String[] args) throws Exception {
+ String client = System.getProperty("client", "valkeyglide");
+ int threads = Integer.parseInt(System.getProperty("threads", "10"));
+ int operations = Integer.parseInt(System.getProperty("operations", "50"));
+ int totalExpected = threads * operations * 2; // SET + GET
+
+ System.out.println("Running Template Load Test");
+ System.out.println("Client: " + client);
+ System.out.println("Threads: " + threads);
+ System.out.println("Operations per thread: " + operations);
+ System.out.println("Total operations: " + totalExpected);
+ System.out.println("----------------------------------------");
+
+ ValkeyConnectionFactory factory = createConnectionFactory(client);
+ if (factory instanceof InitializingBean) {
+ ((InitializingBean) factory).afterPropertiesSet();
+ }
+
+ try {
+ runLoadTest(factory, threads, operations, totalExpected);
+ } finally {
+ if (factory instanceof DisposableBean) {
+ ((DisposableBean) factory).destroy();
+ }
+ }
+ }
+
+ private static ValkeyConnectionFactory createConnectionFactory(String clientType) {
+ return switch (clientType.toLowerCase()) {
+ case "lettuce" -> new LettuceConnectionFactory();
+ case "jedis" -> new JedisConnectionFactory();
+ case "valkeyglide" -> new ValkeyGlideConnectionFactory();
+ default -> throw new IllegalArgumentException("Unknown client: " + clientType);
+ };
+ }
+
+ private static void runLoadTest(ValkeyConnectionFactory factory, int threads,
+ int operations, int totalExpected) throws InterruptedException {
+ long startTime = System.currentTimeMillis();
+
+ ExecutorService executorService = Executors.newFixedThreadPool(threads);
+ StringValkeyTemplate valkeyTemplate = new StringValkeyTemplate(factory);
+
+ AtomicInteger setOperations = new AtomicInteger(0);
+ AtomicInteger getOperations = new AtomicInteger(0);
+
+ try {
+ Runnable task = () -> IntStream.range(0, operations).forEach(i -> {
+ String key = Thread.currentThread().getName() + ":" + i;
+ String value = "value" + i;
+
+ // SET operation
+ try {
+ valkeyTemplate.opsForValue().set(key, value);
+ setOperations.incrementAndGet();
+ } catch (Exception e) {
+ System.err.println("SET error: " + e.getMessage());
+ }
+
+ // GET operation
+ try {
+ String result = valkeyTemplate.opsForValue().get(key);
+ getOperations.incrementAndGet();
+ if (result != null && !result.equals(value)) {
+ System.err.println("Data mismatch! Expected: " + value + ", Got: " + result);
+ }
+ } catch (Exception e) {
+ System.err.println("GET error: " + e.getMessage());
+ }
+ });
+
+ IntStream.range(0, threads).forEach(i -> executorService.submit(task));
+
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+
+ long duration = System.currentTimeMillis() - startTime;
+ int totalActual = setOperations.get() + getOperations.get();
+ int dropped = totalExpected - totalActual;
+
+ System.out.println("Duration: " + String.format("%.2f ms", (double) duration));
+ System.out.println("Expected operations: " + totalExpected);
+ System.out.println("SET operations: " + setOperations.get());
+ System.out.println("GET operations: " + getOperations.get());
+ System.out.println("Total operations: " + totalActual);
+ System.out.println("Dropped operations: " + dropped);
+ System.out.println("Success rate: " + String.format("%.2f%%", (totalActual * 100.0 / totalExpected)));
+ System.out.println("Operations per second: " + String.format("%,d", (long) (totalActual * 1000.0 / duration)));
+ } finally {
+ executorService.shutdown();
+ }
+ }
+}
diff --git a/performance/src/main/java/performance/TemplatePerformanceTest.java b/performance/src/main/java/performance/TemplatePerformanceTest.java
new file mode 100644
index 00000000..00257297
--- /dev/null
+++ b/performance/src/main/java/performance/TemplatePerformanceTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package performance;
+
+import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory;
+import io.valkey.springframework.data.valkey.connection.jedis.JedisConnectionFactory;
+import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory;
+import io.valkey.springframework.data.valkey.connection.valkeyglide.ValkeyGlideConnectionFactory;
+import io.valkey.springframework.data.valkey.core.StringValkeyTemplate;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.DisposableBean;
+
+/**
+ * Performance test for ValkeyTemplate operations across different clients.
+ */
+public class TemplatePerformanceTest {
+
+ private static final int OPERATIONS = 10000;
+ private static final String KEY_PREFIX = "perf:test:";
+
+ public static void main(String[] args) throws Exception {
+ String clientType = System.getProperty("client", "valkeyglide");
+
+ System.out.println("Running ValkeyTemplate Performance Test");
+ System.out.println("Client: " + clientType);
+ System.out.println("Operations: " + OPERATIONS);
+ System.out.println("----------------------------------------");
+
+ ValkeyConnectionFactory factory = createConnectionFactory(clientType);
+ if (factory instanceof InitializingBean) {
+ ((InitializingBean) factory).afterPropertiesSet();
+ }
+
+ try {
+ StringValkeyTemplate template = new StringValkeyTemplate(factory);
+ runPerformanceTest(template);
+ } finally {
+ if (factory instanceof DisposableBean) {
+ ((DisposableBean) factory).destroy();
+ }
+ }
+ }
+
+ private static ValkeyConnectionFactory createConnectionFactory(String clientType) {
+ return switch (clientType.toLowerCase()) {
+ case "lettuce" -> new LettuceConnectionFactory();
+ case "jedis" -> new JedisConnectionFactory();
+ case "valkeyglide" -> new ValkeyGlideConnectionFactory();
+ default -> throw new IllegalArgumentException("Unknown client: " + clientType);
+ };
+ }
+
+ private static void runPerformanceTest(StringValkeyTemplate template) {
+ // SET operations
+ long start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ template.opsForValue().set(KEY_PREFIX + i, "value" + i);
+ }
+ long setTime = System.nanoTime() - start;
+ printResult("SET", setTime);
+
+ // GET operations
+ start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ template.opsForValue().get(KEY_PREFIX + i);
+ }
+ long getTime = System.nanoTime() - start;
+ printResult("GET", getTime);
+
+ // DELETE operations
+ start = System.nanoTime();
+ for (int i = 0; i < OPERATIONS; i++) {
+ template.delete(KEY_PREFIX + i);
+ }
+ long deleteTime = System.nanoTime() - start;
+ printResult("DELETE", deleteTime);
+ }
+
+ private static void printResult(String operation, long durationNanos) {
+ long durationMs = durationNanos / 1_000_000;
+ System.out.printf("%s: %,d ops/sec (%.2f ms total)%n",
+ operation, (long) (OPERATIONS * 1000.0 / durationMs), durationMs / 1.0);
+ }
+}
diff --git a/performance/src/main/java/performance/ThreadedDirectClientPerformanceTest.java b/performance/src/main/java/performance/ThreadedDirectClientPerformanceTest.java
new file mode 100644
index 00000000..fbb6c649
--- /dev/null
+++ b/performance/src/main/java/performance/ThreadedDirectClientPerformanceTest.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package performance;
+
+import glide.api.GlideClient;
+import glide.api.models.GlideString;
+import glide.api.models.configuration.GlideClientConfiguration;
+import glide.api.models.configuration.NodeAddress;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
+import io.lettuce.core.api.StatefulRedisConnection;
+import io.lettuce.core.api.sync.RedisCommands;
+import redis.clients.jedis.Jedis;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Multi-threaded direct client performance test across multiple threads.
+ */
+public class ThreadedDirectClientPerformanceTest {
+
+ private static final int THREADS = 100;
+ private static final int OPERATIONS_PER_THREAD = 100;
+ private static final int TOTAL_OPERATIONS = THREADS * OPERATIONS_PER_THREAD;
+ private static final String KEY_PREFIX = "threaded:test:";
+
+ public static void main(String[] args) throws Exception {
+ String clientType = System.getProperty("client", "valkeyglide");
+
+ System.out.println("Running Multi-Threaded Direct Client Performance Test");
+ System.out.println("Client: " + clientType);
+ System.out.println("Threads: " + THREADS);
+ System.out.println("Operations per thread: " + OPERATIONS_PER_THREAD);
+ System.out.println("Total operations: " + TOTAL_OPERATIONS);
+ System.out.println("----------------------------------------");
+
+ switch (clientType.toLowerCase()) {
+ case "valkeyglide" -> testValkeyGlide();
+ case "lettuce" -> testLettuce();
+ case "jedis" -> testJedis();
+ default -> throw new IllegalArgumentException("Unknown client: " + clientType);
+ }
+ }
+
+ private static void testValkeyGlide() throws Exception {
+ GlideClientConfiguration config = GlideClientConfiguration.builder()
+ .address(NodeAddress.builder().host("localhost").port(6379).build())
+ .build();
+
+ try (GlideClient client = GlideClient.createClient(config).get()) {
+ ExecutorService executorService = Executors.newFixedThreadPool(THREADS);
+ AtomicInteger setSuccess = new AtomicInteger(0);
+ AtomicInteger getSuccess = new AtomicInteger(0);
+ AtomicInteger deleteSuccess = new AtomicInteger(0);
+
+ try {
+ // SET operations
+ long start = System.nanoTime();
+ Runnable setTask = () -> {
+ for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+ try {
+ int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i;
+ client.set(GlideString.of(KEY_PREFIX + id), GlideString.of("value" + id)).get();
+ setSuccess.incrementAndGet();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ };
+
+ for (int i = 0; i < THREADS; i++) {
+ executorService.submit(setTask);
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long setTime = System.nanoTime() - start;
+ printResult("SET", setTime, setSuccess.get());
+
+ // GET operations
+ executorService = Executors.newFixedThreadPool(THREADS);
+ start = System.nanoTime();
+ Runnable getTask = () -> {
+ for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+ try {
+ int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i;
+ client.get(GlideString.of(KEY_PREFIX + id)).get();
+ getSuccess.incrementAndGet();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ };
+
+ for (int i = 0; i < THREADS; i++) {
+ executorService.submit(getTask);
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long getTime = System.nanoTime() - start;
+ printResult("GET", getTime, getSuccess.get());
+
+ // DELETE operations
+ executorService = Executors.newFixedThreadPool(THREADS);
+ start = System.nanoTime();
+ Runnable deleteTask = () -> {
+ for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+ try {
+ int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i;
+ client.del(new GlideString[]{GlideString.of(KEY_PREFIX + id)}).get();
+ deleteSuccess.incrementAndGet();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ };
+
+ for (int i = 0; i < THREADS; i++) {
+ executorService.submit(deleteTask);
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long deleteTime = System.nanoTime() - start;
+ printResult("DELETE", deleteTime, deleteSuccess.get());
+ } finally {
+ if (!executorService.isShutdown()) {
+ executorService.shutdown();
+ }
+ }
+ }
+ }
+
+ private static void testLettuce() throws Exception {
+ RedisClient client = RedisClient.create(RedisURI.create("redis://localhost:6379"));
+
+ try (StatefulRedisConnection connection = client.connect()) {
+ RedisCommands commands = connection.sync();
+ ExecutorService executorService = Executors.newFixedThreadPool(THREADS);
+ AtomicInteger setSuccess = new AtomicInteger(0);
+ AtomicInteger getSuccess = new AtomicInteger(0);
+ AtomicInteger deleteSuccess = new AtomicInteger(0);
+
+ try {
+ // SET operations
+ long start = System.nanoTime();
+ Runnable setTask = () -> {
+ for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+ try {
+ int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i;
+ commands.set(KEY_PREFIX + id, "value" + id);
+ setSuccess.incrementAndGet();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ };
+
+ for (int i = 0; i < THREADS; i++) {
+ executorService.submit(setTask);
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long setTime = System.nanoTime() - start;
+ printResult("SET", setTime, setSuccess.get());
+
+ // GET operations
+ executorService = Executors.newFixedThreadPool(THREADS);
+ start = System.nanoTime();
+ Runnable getTask = () -> {
+ for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+ try {
+ int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i;
+ commands.get(KEY_PREFIX + id);
+ getSuccess.incrementAndGet();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ };
+
+ for (int i = 0; i < THREADS; i++) {
+ executorService.submit(getTask);
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long getTime = System.nanoTime() - start;
+ printResult("GET", getTime, getSuccess.get());
+
+ // DELETE operations
+ executorService = Executors.newFixedThreadPool(THREADS);
+ start = System.nanoTime();
+ Runnable deleteTask = () -> {
+ for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+ try {
+ int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i;
+ commands.del(KEY_PREFIX + id);
+ deleteSuccess.incrementAndGet();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ };
+
+ for (int i = 0; i < THREADS; i++) {
+ executorService.submit(deleteTask);
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long deleteTime = System.nanoTime() - start;
+ printResult("DELETE", deleteTime, deleteSuccess.get());
+ } finally {
+ if (!executorService.isShutdown()) {
+ executorService.shutdown();
+ }
+ }
+ } finally {
+ client.shutdown();
+ }
+ }
+
+ private static void testJedis() throws Exception {
+ // Jedis connections are not thread-safe, so we need one per thread
+ ExecutorService executorService = Executors.newFixedThreadPool(THREADS);
+ AtomicInteger setSuccess = new AtomicInteger(0);
+ AtomicInteger getSuccess = new AtomicInteger(0);
+ AtomicInteger deleteSuccess = new AtomicInteger(0);
+
+ try {
+ // SET operations
+ long start = System.nanoTime();
+ Runnable setTask = () -> {
+ try (Jedis jedis = new Jedis("localhost", 6379)) {
+ for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+ try {
+ int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i;
+ jedis.set(KEY_PREFIX + id, "value" + id);
+ setSuccess.incrementAndGet();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ }
+ };
+
+ for (int i = 0; i < THREADS; i++) {
+ executorService.submit(setTask);
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long setTime = System.nanoTime() - start;
+ printResult("SET", setTime, setSuccess.get());
+
+ // GET operations
+ executorService = Executors.newFixedThreadPool(THREADS);
+ start = System.nanoTime();
+ Runnable getTask = () -> {
+ try (Jedis jedis = new Jedis("localhost", 6379)) {
+ for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+ try {
+ int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i;
+ jedis.get(KEY_PREFIX + id);
+ getSuccess.incrementAndGet();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ }
+ };
+
+ for (int i = 0; i < THREADS; i++) {
+ executorService.submit(getTask);
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long getTime = System.nanoTime() - start;
+ printResult("GET", getTime, getSuccess.get());
+
+ // DELETE operations
+ executorService = Executors.newFixedThreadPool(THREADS);
+ start = System.nanoTime();
+ Runnable deleteTask = () -> {
+ try (Jedis jedis = new Jedis("localhost", 6379)) {
+ for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+ try {
+ int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i;
+ jedis.del(KEY_PREFIX + id);
+ deleteSuccess.incrementAndGet();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ }
+ };
+
+ for (int i = 0; i < THREADS; i++) {
+ executorService.submit(deleteTask);
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, TimeUnit.SECONDS);
+ long deleteTime = System.nanoTime() - start;
+ printResult("DELETE", deleteTime, deleteSuccess.get());
+ } finally {
+ if (!executorService.isShutdown()) {
+ executorService.shutdown();
+ }
+ }
+ }
+
+ private static void printResult(String operation, long duration, int successful) {
+ System.out.printf("%s: %,d ops/sec (%.2f ms total), %.1f%% successful%n",
+ operation, (long) (TOTAL_OPERATIONS / (duration / 1_000_000_000.0)), duration / 1_000_000.0,
+ (successful * 100.0 / TOTAL_OPERATIONS));
+ }
+}