diff --git a/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/README.md b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/README.md
new file mode 100644
index 0000000..61524f3
--- /dev/null
+++ b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/README.md
@@ -0,0 +1,72 @@
+# Use Valkey GLIDE with Spring Boot - ElastiCache Cluster Mode Enabled and IAM Auth
+
+[Valkey Glide](https://github.com/valkey-io/valkey-glide) is the official open-source Valkey client library, proudly part of the Valkey organization.
+
+[Spring Boot](https://docs.spring.io/spring-boot/index.html) helps you to create stand-alone, production-grade Spring-based applications that you can run. Spring Boot takes an opinionated view of the Spring platform and third-party libraries, so that you can get started with minimum fuss. It contains a [caching](https://docs.spring.io/spring-boot/reference/io/caching.html) component, making implementation of caching in your application quick and simple.
+
+This demo will show you a simple way to implement the GLIDE client with Spring Boot caching. The target ElastiCache cluster will have cluster mode enabled and use IAM Authentication.
+
+## Prerequesites
+
+To build the application, you must have the following prerequisites.
+
+Some compute with the following installed:
+- *Java 17* - To install the Java Development Kit (JDK) 17, run `sudo yum install -y java-17-amazon-corretto-devel` on your EC2 instance
+- *Maven* - To install Apache Maven, run `sudo yum install -y maven` on your EC2 instance
+
+The compute must have access to Redis OSS or Valkey ElastiCache cluster with IAM Authentication configured.
+
+## Running
+
+To run the demo application, update `application.properties` to point to Redis or Valkey. Also update the `username`, `cachename` and `region` for your ElastiCache cluster.
+
+`spring.data.valkey.host=cache1-XXXXX.serverless.euw2.cache.amazonaws.com`
+
+Run `mvn spring-boot:run` in the root folder.
+
+See the results!
+
+## How it works
+
+To use Valkey Glide, we add 3 classes to the application:
+
+`SimpleValkeyGlideCache.java`. This class implements the Spring Framework Cache interface. It is simple in nature - all cache key objects must have the `toString()` method, and all cached objects must implement `java.lang.Serializable`. This class adapts Spring Framework to use the GLIDE client.
+
+`ValkeyGlideCacheManager.java`. This class implements Spring Framework `org.springframework.cache.CacheManager` interface and instantiates a Bean Component. On startup, Spring Framework looks for a Bean implementing this interface and uses it to create and manage access to caches at runtime. This class uses `SimpleValkeyGlideCache` for caches.
+
+`ElastiCachePasswordGenerator.java`. This class creates passwords for the ElastiCache cluster using IAM.
+
+The Maven POM file contains three dependencies:
+
+```
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+ io.valkey
+ valkey-glide
+ ${os.detected.classifier}
+ [1.0.0,2.0.0)
+
+
+ software.amazon.awssdk
+ auth
+ 2.32.2
+
+```
+
+`spring-boot-starter-cache` tells Spring Framework to implement caching.
+
+`valkey-glide` includes the Valkey GLIDE client in your application. See the GLIDE [client documentation](https://valkey.io/clients/) for more details.
+
+`software.amazon.awssdk.auth` provides utility classes for password creation using IAM.
+
+## Want to try this with your existing Spring Boot application?
+
+1. Remove any existing Caching providers from your dependencies (i.e. `spring-boot-starter-cache-redis`).
+2. Add `SimpleValkeyGlideCache.java`, `ElastiCachePasswordGenerator.java` and `ValkeyGlideCacheManager.java` to your application.
+3. Modify any entries in your `application.properties` that configure Redis, changing them to valkey (i.e. `spring.data.redis.host` becomes `spring.data.valkey.host`).
+4. add `spring.data.valkey.username`, `spring.data.valkey.cacheName` and `spring.data.valkey.region` to `application.properties` with relevant values.
+
+Give your application a try!
\ No newline at end of file
diff --git a/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/pom.xml b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/pom.xml
new file mode 100644
index 0000000..3c89a1b
--- /dev/null
+++ b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/pom.xml
@@ -0,0 +1,52 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.3
+
+
+ com.example
+ elasticache-demo
+ 0.0.1-SNAPSHOT
+ elasticache-demo
+ Demo project for Spring Boot with Elasticache
+
+ 17
+
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+ io.valkey
+ valkey-glide
+ ${os.detected.classifier}
+ [1.0.0,2.0.0)
+
+
+ software.amazon.awssdk
+ auth
+ 2.32.2
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/AppRunner.java b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/AppRunner.java
new file mode 100644
index 0000000..b3787a1
--- /dev/null
+++ b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/AppRunner.java
@@ -0,0 +1,28 @@
+package com.example.elasticache_demo;
+
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+@Component
+public class AppRunner implements CommandLineRunner {
+
+ private static final long TEN_MINUTES = 60 * 10 * 1000;
+ private static final long ONE_HOUR_MILLIS = TEN_MINUTES * 6;
+ private static final long TWENTY_HOUR_MILLIS = ONE_HOUR_MILLIS * 20;
+
+ private CacheableComponent cacheableComponent;
+
+ public AppRunner(CacheableComponent cacheableComponent) {
+ this.cacheableComponent = cacheableComponent;
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+ long started = System.currentTimeMillis();
+ while ( System.currentTimeMillis() < started + TWENTY_HOUR_MILLIS ) {
+ String newValue = cacheableComponent.getCacheableValue("test-key" + System.currentTimeMillis());
+ System.out.println("READ " + newValue);
+ Thread.sleep(TEN_MINUTES);
+ }
+ }
+}
\ No newline at end of file
diff --git a/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/CacheableComponent.java b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/CacheableComponent.java
new file mode 100644
index 0000000..9811cf3
--- /dev/null
+++ b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/CacheableComponent.java
@@ -0,0 +1,13 @@
+package com.example.elasticache_demo;
+
+import org.springframework.stereotype.Component;
+import org.springframework.cache.annotation.Cacheable;
+
+@Component
+public class CacheableComponent {
+
+ @Cacheable("test")
+ public String getCacheableValue(String key) throws Exception {
+ return key + System.currentTimeMillis();
+ }
+}
diff --git a/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/ElasticacheDemoApplication.java b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/ElasticacheDemoApplication.java
new file mode 100644
index 0000000..9396011
--- /dev/null
+++ b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/ElasticacheDemoApplication.java
@@ -0,0 +1,16 @@
+package com.example.elasticache_demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.beans.factory.annotation.Autowired;
+
+@SpringBootApplication
+@EnableCaching
+public class ElasticacheDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ElasticacheDemoApplication.class, args);
+ }
+
+}
diff --git a/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/ElasticachePasswordGenerator.java b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/ElasticachePasswordGenerator.java
new file mode 100644
index 0000000..c4da4b7
--- /dev/null
+++ b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/ElasticachePasswordGenerator.java
@@ -0,0 +1,83 @@
+package com.example.elasticache_demo;
+
+import java.net.URI;
+import java.time.Duration;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.http.SdkHttpMethod;
+import software.amazon.awssdk.http.SdkHttpRequest;
+import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner.AuthLocation;
+import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
+import software.amazon.awssdk.utils.StringUtils;
+
+// example class from https://github.com/valkey-io/valkey-glide/wiki/Java-Wrapper#example---using-iam-authentication-with-glide-for-elasticache-and-memorydb
+class ElasticachePasswordGenerator {
+ private final AwsV4HttpSigner awsV4HttpSigner;
+ private final AwsCredentialsProvider credentialsProvider;
+
+ private final String cacheName;
+ private final String cacheRegion;
+ private final String userId;
+ private final boolean isServerless;
+
+ private static final String FAKE_SCHEME = "https://";
+
+ ElasticachePasswordGenerator(
+ final String cacheName,
+ final String cacheRegion,
+ final String userId,
+ final AwsV4HttpSigner awsV4HttpSigner,
+ final AwsCredentialsProvider credentialsProvider,
+ final boolean isServerless) {
+
+ this.cacheName = cacheName;
+ this.cacheRegion = cacheRegion;
+ this.userId = userId;
+ this.awsV4HttpSigner = awsV4HttpSigner;
+ this.credentialsProvider = credentialsProvider;
+ this.isServerless = isServerless;
+ }
+
+ public static ElasticachePasswordGenerator create(
+ final String cacheName, final String cacheRegion, final String userId, final boolean isServerless) {
+ if (StringUtils.isEmpty(cacheName)) {
+ throw new IllegalArgumentException("cacheName must be provided");
+ }
+
+ if (StringUtils.isEmpty(cacheRegion)) {
+ throw new IllegalArgumentException("cacheRegion must be provided");
+ }
+
+ if (StringUtils.isEmpty(userId)) {
+ throw new IllegalArgumentException("userId must be provided");
+ }
+
+ return new ElasticachePasswordGenerator(
+ cacheName, cacheRegion, userId, AwsV4HttpSigner.create(), DefaultCredentialsProvider.create(), isServerless);
+ }
+
+ public String generatePassword() {
+ final var requestUri = URI.create(String.format("%s%s/", FAKE_SCHEME, cacheName));
+ final var requestBuilder = SdkHttpRequest.builder()
+ .method(SdkHttpMethod.GET)
+ .uri(requestUri)
+ .appendRawQueryParameter("Action", "connect")
+ .appendRawQueryParameter("User", userId);
+ if (this.isServerless) {
+ requestBuilder.appendRawQueryParameter("ResourceType", "ServerlessCache");
+ }
+
+ final var cacheRequest = requestBuilder.build();
+
+ final var signedRequest = awsV4HttpSigner.sign(signRequest -> signRequest
+ .request(cacheRequest)
+ .identity(credentialsProvider.resolveCredentials())
+ .putProperty(AwsV4HttpSigner.EXPIRATION_DURATION, Duration.ofMinutes(15))
+ .putProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, "elasticache")
+ .putProperty(AwsV4HttpSigner.REGION_NAME, cacheRegion)
+ .putProperty(AwsV4HttpSigner.AUTH_LOCATION, AuthLocation.QUERY_STRING));
+
+ String password = signedRequest.request().getUri().toString().replace(FAKE_SCHEME, "");
+ return password;
+ }
+}
\ No newline at end of file
diff --git a/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/SimpleValkeyGlideCache.java b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/SimpleValkeyGlideCache.java
new file mode 100644
index 0000000..3fda995
--- /dev/null
+++ b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/SimpleValkeyGlideCache.java
@@ -0,0 +1,219 @@
+package com.example.elasticache_demo;
+
+import org.springframework.cache.Cache;
+import org.springframework.cache.support.SimpleValueWrapper;
+import glide.api.GlideClusterClient;
+import glide.api.models.configuration.NodeAddress;
+import glide.api.models.configuration.GlideClusterClientConfiguration;
+import glide.api.models.commands.SetOptions;
+import glide.api.models.commands.SetOptions.SetOptionsBuilder;
+import glide.api.models.commands.scan.ScanOptions;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import glide.api.models.commands.scan.ClusterScanCursor;
+
+import java.io.*;
+
+import java.util.Base64;
+import java.time.Duration;
+
+import glide.api.models.GlideString;
+import static glide.api.models.GlideString.gs;
+
+/*
+ * A simple Valkey Glide implementation to be able to handle any cache entry, as long as the key has a
+ * toString() method, and the value implements java.io.Serializable
+ */
+public class SimpleValkeyGlideCache implements Cache {
+
+ private String name;
+ private Duration ttl;
+ private GlideClusterClient client;
+
+ public SimpleValkeyGlideCache(String name, GlideClusterClientConfiguration config, Duration ttl) {
+ try {
+ this.name = name;
+ this.ttl = ttl;
+ this.client = GlideClusterClient.createClient(config).get();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create cache " + name, e);
+ }
+ }
+
+ public void updatePassword(String password, boolean immediate) {
+ this.client.updateConnectionPassword(password, immediate);
+ }
+
+ private String buildKey(Object key) {
+ // prefix the cache name to the key in order to avoid clashes
+ return name + "::" + key.toString();
+ }
+
+ // this works for objects that implement java.io.Serializable
+ private String writeValue(Object value) throws java.io.IOException {
+ if (value == null) {
+ return "";
+ }
+
+ ByteArrayOutputStream serialObj = new ByteArrayOutputStream();
+ ObjectOutputStream objStream = new ObjectOutputStream(serialObj);
+ objStream.writeObject(value);
+ objStream.close();
+
+ return Base64.getEncoder().encodeToString(serialObj.toByteArray());
+ }
+
+ // this works for objects that implement java.io.Serializable
+ private Object readValue(String value) throws java.io.IOException, java.lang.ClassNotFoundException {
+ if (value == null || value.length() == 0) return null;
+
+ ByteArrayInputStream serialObj = new ByteArrayInputStream(Base64.getDecoder().decode((value)));
+ ObjectInputStream objStream = new ObjectInputStream(serialObj);
+
+ return objStream.readObject();
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public Object getNativeCache() {
+ return this.client;
+ }
+
+ @Override
+ public ValueWrapper get(Object key) {
+ if (key == null) {
+ return null;
+ }
+
+ try {
+ Object value = readValue(client.get(buildKey(key)).get());
+ return (value != null ? new SimpleValueWrapper(value) : null);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to get value from cache", e);
+ }
+ }
+
+ @Override
+ public T get(Object key, Class type) {
+ ValueWrapper wrapper = get(key);
+ if (wrapper == null) {
+ return null;
+ }
+
+ Object value = wrapper.get();
+ if (value != null && type != null && !type.isInstance(value)) {
+ throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
+ }
+ return (T) value;
+ }
+
+ @Override
+ public T get(Object key, Callable valueLoader) {
+ ValueWrapper wrapper = get(key);
+ if (wrapper != null) {
+ return (T) wrapper.get();
+ }
+
+ T value;
+ try {
+ value = valueLoader.call();
+ } catch (Exception ex) {
+ throw new ValueRetrievalException(key, valueLoader, ex);
+ }
+
+ put(key, value);
+ return value;
+ }
+
+ @Override
+ public void put(Object key, Object value) {
+ putWithOptions(key, value, SetOptions.builder());
+ }
+
+ @Override
+ public Cache.ValueWrapper putIfAbsent(Object key, Object value) {
+ ValueWrapper existingValue = get(key);
+ if (existingValue == null) {
+ putWithOptions(key, value, SetOptions.builder().conditionalSet(SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST));
+ }
+ return existingValue;
+ }
+
+ private void putWithOptions(Object key, Object value, SetOptionsBuilder options) {
+ if ( key == null ) {
+ return;
+ }
+
+ try {
+ if ( this.ttl != null ) {
+ options.expiry(SetOptions.Expiry.Milliseconds(System.currentTimeMillis() + ttl.toMillis()));
+ }
+
+ client.set(buildKey(key), writeValue(value), options.build()).get();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to put value in cache", e);
+ }
+ }
+
+ @Override
+ public void evict(Object key) {
+ evictIfPresent(buildKey(key));
+ }
+
+ @Override
+ public boolean evictIfPresent(Object key) {
+ if ( key != null ) {
+ try {
+ return !client.getdel(buildKey(key)).get().equals(null);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to evict key from cache", e);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ try {
+ // we could scan and unlink, but for simplicity just call invalidate
+ invalidate();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to clear cache", e);
+ }
+ }
+
+ @Override
+ public boolean invalidate() {
+ long count = 0;
+
+ try {
+ ClusterScanCursor cursor = ClusterScanCursor.initalCursor();
+ while (!cursor.isFinished()) {
+ final Object[] response = client.scan(
+ cursor,
+ ScanOptions.builder()
+ .matchPattern(name + "::*")
+ .type(ScanOptions.ObjectType.STRING)
+ .build()
+ ).get();
+ cursor.releaseCursorHandle();
+
+ cursor = (ClusterScanCursor) response[0];
+ final Object[] keys = (Object[]) response[1];
+ String[] stringKeys = java.util.Arrays.stream(keys).map(obj -> obj.toString()).collect(java.util.stream.Collectors.joining(", ")).split(", ");
+ count += stringKeys.length;
+
+ client.del(stringKeys).get();
+ }
+ cursor.releaseCursorHandle();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to invalidate cache", e);
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/ValkeyGlideCacheManager.java b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/ValkeyGlideCacheManager.java
new file mode 100644
index 0000000..e45f25d
--- /dev/null
+++ b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/java/com/example/elasticache_demo/ValkeyGlideCacheManager.java
@@ -0,0 +1,95 @@
+package com.example.elasticache_demo;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.Cache;
+
+import org.springframework.boot.convert.DurationStyle;
+import org.springframework.util.StringUtils;
+
+import glide.api.models.configuration.ServerCredentials;
+import glide.api.models.configuration.NodeAddress;
+import glide.api.models.configuration.GlideClusterClientConfiguration;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Collection;
+import java.time.Duration;
+
+@Component
+public class ValkeyGlideCacheManager implements CacheManager {
+
+ @Value( "${spring.cache.valkey.time-to-live:}" )
+ private String timeToLive;
+
+ @Value( "${spring.data.valkey.ssl.enabled:false}" )
+ private Boolean useTLS;
+
+ @Value( "${spring.data.valkey.host:127.0.0.1}" )
+ private String host;
+
+ @Value( "${spring.cache.valkey.port:6379}" )
+ private Integer port;
+
+ @Value("${spring.data.valkey.username}")
+ private String username;
+
+ @Value("${spring.data.valkey.cacheName}")
+ private String cacheName;
+
+ @Value("${spring.data.valkey.region}")
+ private String region;
+
+ private Map caches = new HashMap();
+ private Map cacheReauthenticated = new HashMap();
+
+ private GlideClusterClientConfiguration config;
+ private Duration ttlDuration;
+ private ElasticachePasswordGenerator generator;
+
+ private static final long TEN_HOURS = 60 * 60 * 10 * 1000;
+
+ public void setCacheConfiguration(GlideClusterClientConfiguration config) {
+ this.config = config;
+ }
+
+ public Cache getCache(String name) {
+ if (!caches.containsKey(name)) {
+ if( generator == null ) {
+ generator = ElasticachePasswordGenerator.create(cacheName, region, username, false);
+ }
+
+ if (config == null) {
+ ServerCredentials credentials = ServerCredentials.builder()
+ .username(username)
+ .password(generator.generatePassword())
+ .build();
+
+ config = GlideClusterClientConfiguration.builder()
+ .address(NodeAddress.builder().host(host).port(port).build())
+ .useTLS(useTLS)
+ .credentials(credentials)
+ .build();
+
+ if (ttlDuration == null && !StringUtils.isEmpty(timeToLive)) {
+ ttlDuration = DurationStyle.detectAndParse(timeToLive);
+ }
+ }
+
+ caches.put(name, new SimpleValkeyGlideCache(name, config, ttlDuration));
+ cacheReauthenticated.put(name, System.currentTimeMillis());
+ }
+
+ if(cacheReauthenticated.get(name) > System.currentTimeMillis() + TEN_HOURS) {
+ caches.get(name).updatePassword(generator.generatePassword(), true);
+ cacheReauthenticated.put(name, System.currentTimeMillis());
+ }
+
+ return caches.get(name);
+ }
+
+ public Collection getCacheNames() {
+ return caches.keySet();
+ }
+}
\ No newline at end of file
diff --git a/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/resources/application.properties b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/resources/application.properties
new file mode 100644
index 0000000..8e3d424
--- /dev/null
+++ b/glide-samples/spring-framework-glide-example-cluster-mode-with-iam/src/main/resources/application.properties
@@ -0,0 +1,7 @@
+spring.data.valkey.ssl.enabled=false
+spring.data.valkey.host=
+spring.cache.valkey.time-to-live=10m
+
+spring.data.valkey.username=
+spring.data.valkey.cacheName=
+spring.data.valkey.region=
\ No newline at end of file