This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 5a29c8a2e742 CAMEL-24463: Add KeyValueRepository implementations for 
Caffeine, Ehcache, JCache, and Hazelcast (#25816)
5a29c8a2e742 is described below

commit 5a29c8a2e74227e4f28938302764f8e85a641be3
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Sep 2 10:42:54 2026 +0200

    CAMEL-24463: Add KeyValueRepository implementations for Caffeine, Ehcache, 
JCache, and Hazelcast (#25816)
    
    CAMEL-24463: Add KeyValueRepository implementations for Caffeine, Ehcache, 
JCache, and Hazelcast
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../org/apache/camel/catalog/beans.properties      |   4 +
 .../catalog/beans/CaffeineKeyValueRepository.json  |  16 ++
 .../catalog/beans/EhcacheKeyValueRepository.json   |  16 ++
 .../catalog/beans/HazelcastKeyValueRepository.json |  16 ++
 .../catalog/beans/JCacheKeyValueRepository.json    |  16 ++
 components/camel-caffeine/pom.xml                  |  11 +
 .../CaffeineKeyValueRepositoryConfigurer.java      |  51 ++++
 .../services/org/apache/camel/bean.properties      |   2 +-
 .../camel/bean/CaffeineKeyValueRepository.json     |  16 ++
 ...t.caffeine.processor.CaffeineKeyValueRepository |   2 +
 .../processor/CaffeineKeyValueRepository.java      | 228 ++++++++++++++++
 .../processor/CaffeineKeyValueRepositoryTest.java  | 303 +++++++++++++++++++++
 components/camel-ehcache/pom.xml                   |  11 +
 .../EhcacheKeyValueRepositoryConfigurer.java       |  57 ++++
 .../services/org/apache/camel/bean.properties      |   2 +-
 .../camel/bean/EhcacheKeyValueRepository.json      |  16 ++
 ...ent.ehcache.processor.EhcacheKeyValueRepository |   2 +
 .../processor/EhcacheKeyValueRepository.java       | 225 +++++++++++++++
 .../processor/EhcacheKeyValueRepositoryTest.java   | 269 ++++++++++++++++++
 components/camel-hazelcast/pom.xml                 |   6 +
 .../HazelcastKeyValueRepositoryConfigurer.java     |  57 ++++
 .../services/org/apache/camel/bean.properties      |   2 +-
 .../camel/bean/HazelcastKeyValueRepository.json    |  16 ++
 ...component.hazelcast.HazelcastKeyValueRepository |   2 +
 .../hazelcast/HazelcastKeyValueRepository.java     | 226 +++++++++++++++
 .../hazelcast/HazelcastKeyValueRepositoryTest.java | 300 ++++++++++++++++++++
 components/camel-jcache/pom.xml                    |  11 +
 .../JCacheKeyValueRepositoryConfigurer.java        |  54 ++++
 .../services/org/apache/camel/bean.properties      |   2 +-
 .../camel/bean/JCacheKeyValueRepository.json       |  16 ++
 ...onent.jcache.processor.JCacheKeyValueRepository |   2 +
 .../jcache/processor/JCacheKeyValueRepository.java | 236 ++++++++++++++++
 .../processor/JCacheKeyValueRepositoryTest.java    | 269 ++++++++++++++++++
 33 files changed, 2458 insertions(+), 4 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
index a7cd67fa6ec4..24bfb7f4b7ac 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans.properties
@@ -1,6 +1,7 @@
 AcceptAllHeaderFilterStrategy
 CaffeineAggregationRepository
 CaffeineIdempotentRepository
+CaffeineKeyValueRepository
 CassandraAggregationRepository
 CassandraIdempotentRepository
 ConsulClusterService
@@ -10,6 +11,7 @@ DurationRoutePolicy
 DurationRoutePolicyFactory
 EhcacheAggregationRepository
 EhcacheIdempotentRepository
+EhcacheKeyValueRepository
 ElasticsearchBulkRequestAggregationStrategy
 FileIdempotentRepository
 FileLockClusterService
@@ -18,6 +20,7 @@ GroupedExchangeAggregationStrategy
 GroupedMessageAggregationStrategy
 HazelcastAggregationRepository
 HazelcastIdempotentRepository
+HazelcastKeyValueRepository
 InfinispanEmbeddedAggregationRepository
 InfinispanEmbeddedClusterService
 InfinispanEmbeddedIdempotentRepository
@@ -27,6 +30,7 @@ InfinispanRemoteIdempotentRepository
 InfinispanRemoteKeyValueRepository
 JCacheAggregationRepository
 JCacheIdempotentRepository
+JCacheKeyValueRepository
 JGroupsRaftClusterService
 JdbcAggregationRepository
 JdbcMessageIdRepository
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/CaffeineKeyValueRepository.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/CaffeineKeyValueRepository.json
new file mode 100644
index 000000000000..be8644b3e7cf
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/CaffeineKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "CaffeineKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Caffeine Key Value Repository",
+    "description": "A Caffeine-based KeyValueRepository with per-entry TTL 
support.",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-caffeine",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "maximumSize": { "index": 0, "kind": "property", 
"displayName": "Maximum Size", "required": false, "type": "integer", 
"javaType": "int", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": 0, "description": "Maximum number of entries in the cache. 0 or 
negative means unbounded." } }
+  }
+}
+
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/EhcacheKeyValueRepository.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/EhcacheKeyValueRepository.json
new file mode 100644
index 000000000000..1d137c8f34d5
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/EhcacheKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "EhcacheKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Ehcache Key Value Repository",
+    "description": "An Ehcache-based KeyValueRepository with per-entry TTL 
support.",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-ehcache",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "cacheName": { "index": 0, "kind": "property", 
"displayName": "Cache Name", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": "EhcacheKeyValueRepository", "description": "Name of cache" }, 
"cacheManager": { "index": 1, "kind": "property", "displayName": "Cache 
Manager", "required": false, "type": "object", "javaType": 
"org.ehcache.CacheManager", "deprecated": false, "autowired [...]
+  }
+}
+
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/HazelcastKeyValueRepository.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/HazelcastKeyValueRepository.json
new file mode 100644
index 000000000000..22d363a3f59c
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/HazelcastKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "HazelcastKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.hazelcast.HazelcastKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Hazelcast Key Value Repository",
+    "description": "A Hazelcast-based KeyValueRepository with native per-entry 
TTL support.",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-hazelcast",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "mapName": { "index": 0, "kind": "property", 
"displayName": "Map Name", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": "HazelcastKeyValueRepository", "description": "Name of the 
Hazelcast map to use" }, "hazelcastInstance": { "index": 1, "kind": "property", 
"displayName": "Hazelcast Instance", "required": false, "type": "object", 
"javaType": "com.hazelcast.core.HazelcastIns [...]
+  }
+}
+
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/JCacheKeyValueRepository.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/JCacheKeyValueRepository.json
new file mode 100644
index 000000000000..3150cfb87a19
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/beans/JCacheKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "JCacheKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.jcache.processor.JCacheKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "JCache Key Value Repository",
+    "description": "A JCache-based KeyValueRepository with per-entry TTL 
support.",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-jcache",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "configuration": { "index": 0, "kind": "property", 
"displayName": "Configuration", "required": false, "type": "object", 
"javaType": "org.apache.camel.component.jcache.JCacheConfiguration", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"Configuration for JCache" } }
+  }
+}
+
diff --git a/components/camel-caffeine/pom.xml 
b/components/camel-caffeine/pom.xml
index b0af285493b6..5d2cb25228b6 100644
--- a/components/camel-caffeine/pom.xml
+++ b/components/camel-caffeine/pom.xml
@@ -51,6 +51,17 @@
             <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>${awaitility-version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-test-spring-junit6</artifactId>
diff --git 
a/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryConfigurer.java
 
b/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryConfigurer.java
new file mode 100644
index 000000000000..0ad0789e29bc
--- /dev/null
+++ 
b/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryConfigurer.java
@@ -0,0 +1,51 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.caffeine.processor;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import 
org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class CaffeineKeyValueRepositoryConfigurer extends 
org.apache.camel.support.component.PropertyConfigurerSupport implements 
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+    @Override
+    public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
+        
org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository target 
= (org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository) 
obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "maximumsize":
+        case "maximumSize": target.setMaximumSize(property(camelContext, 
int.class, value)); return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "maximumsize":
+        case "maximumSize": return int.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        
org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository target 
= (org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository) 
obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "maximumsize":
+        case "maximumSize": return target.getMaximumSize();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
 
b/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
index 0d8d2081b358..b69e2a390cf0 100644
--- 
a/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
+++ 
b/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-bean=CaffeineAggregationRepository CaffeineIdempotentRepository
+bean=CaffeineAggregationRepository CaffeineIdempotentRepository 
CaffeineKeyValueRepository
 groupId=org.apache.camel
 artifactId=camel-caffeine
 version=4.23.0-SNAPSHOT
diff --git 
a/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/bean/CaffeineKeyValueRepository.json
 
b/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/bean/CaffeineKeyValueRepository.json
new file mode 100644
index 000000000000..be8644b3e7cf
--- /dev/null
+++ 
b/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/bean/CaffeineKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "CaffeineKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Caffeine Key Value Repository",
+    "description": "A Caffeine-based KeyValueRepository with per-entry TTL 
support.",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-caffeine",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "maximumSize": { "index": 0, "kind": "property", 
"displayName": "Maximum Size", "required": false, "type": "integer", 
"javaType": "int", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": 0, "description": "Maximum number of entries in the cache. 0 or 
negative means unbounded." } }
+  }
+}
+
diff --git 
a/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository
 
b/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository
new file mode 100644
index 000000000000..fe577a069252
--- /dev/null
+++ 
b/components/camel-caffeine/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepository
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.caffeine.processor.CaffeineKeyValueRepositoryConfigurer
diff --git 
a/components/camel-caffeine/src/main/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepository.java
 
b/components/camel-caffeine/src/main/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepository.java
new file mode 100644
index 000000000000..9b38ef707b22
--- /dev/null
+++ 
b/components/camel-caffeine/src/main/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepository.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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 org.apache.camel.component.caffeine.processor;
+
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Expiry;
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.service.ServiceSupport;
+
+/**
+ * A {@link KeyValueRepository} implementation backed by a Caffeine {@link 
Cache} with per-entry TTL support.
+ * <p/>
+ * Caffeine's {@link Expiry} API is used to assign a TTL to each entry 
individually. Entries without a TTL (null, zero,
+ * or negative duration) are stored with {@link Long#MAX_VALUE} as their 
duration so they effectively never expire.
+ * <p/>
+ * This single implementation can serve as idempotent repository, aggregation 
repository, and state store via the
+ * adapters in {@code camel-support} ({@code KeyValueIdempotentRepository} and 
{@code KeyValueAggregationRepository}).
+ *
+ * @since 4.23
+ */
+@Metadata(label = "bean",
+          description = "A Caffeine-based KeyValueRepository with per-entry 
TTL support.",
+          annotations = { 
"interfaceName=org.apache.camel.spi.KeyValueRepository" })
+@Configurer(metadataOnly = true)
+@ManagedResource(description = "Caffeine based key-value repository")
+public class CaffeineKeyValueRepository extends ServiceSupport implements 
KeyValueRepository {
+
+    /**
+     * Internal value wrapper that carries the per-entry TTL so the custom 
{@link Expiry} can read it at insertion time.
+     */
+    private static final class TtlValue {
+
+        private final Object value;
+        private final long ttlNanos;
+
+        TtlValue(Object value, long ttlNanos) {
+            this.value = value;
+            this.ttlNanos = ttlNanos;
+        }
+
+        Object value() {
+            return value;
+        }
+
+        long ttlNanos() {
+            return ttlNanos;
+        }
+    }
+
+    private Cache<String, TtlValue> cache;
+
+    @Metadata(description = "Maximum number of entries in the cache. 0 or 
negative means unbounded.",
+              defaultValue = "0")
+    private int maximumSize;
+
+    /**
+     * Creates a new Caffeine-backed key-value repository with default 
settings (unbounded, no global TTL).
+     */
+    public CaffeineKeyValueRepository() {
+    }
+
+    /**
+     * Creates a new Caffeine-backed key-value repository wrapping a pre-built 
cache.
+     * <p/>
+     * This is primarily for testing. The supplied cache must use {@link 
TtlValue} as its value type.
+     */
+    CaffeineKeyValueRepository(Cache<String, TtlValue> cache) {
+        this.cache = cache;
+    }
+
+    public int getMaximumSize() {
+        return maximumSize;
+    }
+
+    /**
+     * Sets the maximum number of entries in the cache. 0 or negative means 
unbounded.
+     */
+    public void setMaximumSize(int maximumSize) {
+        this.maximumSize = maximumSize;
+    }
+
+    @Override
+    @ManagedOperation(description = "Get value by key")
+    public Object get(String key) {
+        TtlValue entry = cache.getIfPresent(key);
+        return entry != null ? entry.value() : null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Put a key-value pair with optional TTL")
+    public Object put(String key, Object value, Duration ttl) {
+        long ttlNanos = hasPositiveTtl(ttl) ? ttl.toNanos() : Long.MAX_VALUE;
+        TtlValue previous = cache.asMap().put(key, new TtlValue(value, 
ttlNanos));
+        return previous != null ? previous.value() : null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Delete a key")
+    public Object delete(String key) {
+        TtlValue previous = cache.asMap().remove(key);
+        return previous != null ? previous.value() : null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Check if key exists")
+    public boolean contains(String key) {
+        return cache.getIfPresent(key) != null;
+    }
+
+    @Override
+    public Set<String> keys() {
+        cache.cleanUp();
+        return Set.copyOf(cache.asMap().keySet());
+    }
+
+    @Override
+    @ManagedOperation(description = "Clear all entries")
+    public void clear() {
+        cache.invalidateAll();
+    }
+
+    @Override
+    public Object putIfAbsent(String key, Object value, Duration ttl) {
+        long ttlNanos = hasPositiveTtl(ttl) ? ttl.toNanos() : Long.MAX_VALUE;
+        ConcurrentMap<String, TtlValue> map = cache.asMap();
+        TtlValue newEntry = new TtlValue(value, ttlNanos);
+        TtlValue existing = map.putIfAbsent(key, newEntry);
+        return existing != null ? existing.value() : null;
+    }
+
+    @Override
+    public boolean replace(String key, Object expectedOldValue, Object 
newValue, Duration ttl) {
+        long ttlNanos = hasPositiveTtl(ttl) ? ttl.toNanos() : Long.MAX_VALUE;
+        boolean[] replaced = { false };
+        cache.asMap().computeIfPresent(key, (k, current) -> {
+            if (Objects.equals(current.value(), expectedOldValue)) {
+                replaced[0] = true;
+                return new TtlValue(newValue, ttlNanos);
+            }
+            return current;
+        });
+        return replaced[0];
+    }
+
+    @Override
+    public boolean delete(String key, Object expectedValue) {
+        boolean[] removed = { false };
+        cache.asMap().computeIfPresent(key, (k, current) -> {
+            if (Objects.equals(current.value(), expectedValue)) {
+                removed[0] = true;
+                return null; // returning null removes the entry
+            }
+            return current;
+        });
+        return removed[0];
+    }
+
+    @Override
+    @ManagedAttribute(description = "The number of entries in the repository")
+    public int size() {
+        cache.cleanUp();
+        return (int) cache.estimatedSize();
+    }
+
+    private static boolean hasPositiveTtl(Duration ttl) {
+        return ttl != null && !ttl.isZero() && !ttl.isNegative();
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        if (cache == null) {
+            Caffeine<Object, Object> builder = Caffeine.newBuilder();
+            if (maximumSize > 0) {
+                builder.maximumSize(maximumSize);
+            }
+            // Use a custom Expiry to support per-entry TTL
+            builder.expireAfter(new Expiry<String, TtlValue>() {
+                @Override
+                public long expireAfterCreate(String key, TtlValue value, long 
currentTime) {
+                    return value.ttlNanos();
+                }
+
+                @Override
+                public long expireAfterUpdate(String key, TtlValue value, long 
currentTime, long currentDuration) {
+                    return value.ttlNanos();
+                }
+
+                @Override
+                public long expireAfterRead(String key, TtlValue value, long 
currentTime, long currentDuration) {
+                    return currentDuration;
+                }
+            });
+            cache = builder.build();
+        }
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        if (cache != null) {
+            cache.invalidateAll();
+        }
+    }
+}
diff --git 
a/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryTest.java
 
b/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryTest.java
new file mode 100644
index 000000000000..fe70ec6d794b
--- /dev/null
+++ 
b/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/processor/CaffeineKeyValueRepositoryTest.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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 org.apache.camel.component.caffeine.processor;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class CaffeineKeyValueRepositoryTest {
+
+    private CaffeineKeyValueRepository repository;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        repository = new CaffeineKeyValueRepository();
+        repository.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        repository.stop();
+    }
+
+    @Test
+    void testPutAndGet() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testGetMissingKeyReturnsNull() {
+        assertThat(repository.get("nonexistent")).isNull();
+    }
+
+    @Test
+    void testPutOverwritesExistingValue() {
+        repository.put("key1", "value1", null);
+        repository.put("key1", "value2", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testPutReturnsPreviousValue() {
+        repository.put("key1", "value1", null);
+        Object previous = repository.put("key1", "value2", null);
+
+        assertThat(previous).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutReturnsNullForNewKey() {
+        Object previous = repository.put("key1", "value1", null);
+
+        assertThat(previous).isNull();
+    }
+
+    @Test
+    void testDelete() {
+        repository.put("key1", "value1", null);
+
+        Object deleted = repository.delete("key1");
+
+        assertThat(deleted).isEqualTo("value1");
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteMissingKeyReturnsNull() {
+        assertThat(repository.delete("nonexistent")).isNull();
+    }
+
+    @Test
+    void testContains() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.contains("key1")).isTrue();
+        assertThat(repository.contains("nonexistent")).isFalse();
+    }
+
+    @Test
+    void testKeys() {
+        repository.put("key1", "value1", null);
+        repository.put("key2", "value2", null);
+        repository.put("key3", "value3", null);
+
+        Set<String> keys = repository.keys();
+
+        assertThat(keys).containsExactlyInAnyOrder("key1", "key2", "key3");
+    }
+
+    @Test
+    void testKeysEmpty() {
+        assertThat(repository.keys()).isEmpty();
+    }
+
+    @Test
+    void testClear() {
+        repository.put("key1", "value1", null);
+        repository.put("key2", "value2", null);
+
+        repository.clear();
+
+        assertThat(repository.size()).isZero();
+        assertThat(repository.get("key1")).isNull();
+        assertThat(repository.get("key2")).isNull();
+    }
+
+    @Test
+    void testSize() {
+        assertThat(repository.size()).isZero();
+
+        repository.put("key1", "value1", null);
+        assertThat(repository.size()).isEqualTo(1);
+
+        repository.put("key2", "value2", null);
+        assertThat(repository.size()).isEqualTo(2);
+
+        repository.delete("key1");
+        assertThat(repository.size()).isEqualTo(1);
+    }
+
+    @Test
+    void testPutIfAbsentNewKey() {
+        Object result = repository.putIfAbsent("key1", "value1", null);
+
+        assertThat(result).isNull();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutIfAbsentExistingKey() {
+        repository.put("key1", "value1", null);
+
+        Object result = repository.putIfAbsent("key1", "value2", null);
+
+        assertThat(result).isEqualTo("value1");
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testTtlExpiration() {
+        repository.put("key1", "value1", Duration.ofMillis(100));
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+
+        await().atMost(2, TimeUnit.SECONDS)
+                .untilAsserted(() -> {
+                    assertThat(repository.get("key1")).isNull();
+                    assertThat(repository.contains("key1")).isFalse();
+                });
+    }
+
+    @Test
+    void testTtlExpirationOnKeys() {
+        repository.put("key1", "value1", Duration.ofMillis(100));
+        repository.put("key2", "value2", null); // no expiration
+
+        await().atMost(2, TimeUnit.SECONDS)
+                .untilAsserted(() -> {
+                    assertThat(repository.keys()).containsExactly("key2");
+                    assertThat(repository.size()).isEqualTo(1);
+                });
+    }
+
+    @Test
+    void testNoTtlWithZero() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+    }
+
+    @Test
+    void testNoTtlWithNegative() {
+        repository.put("key1", "value1", Duration.ofMillis(-1));
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+    }
+
+    @Test
+    void testStoresDifferentValueTypes() {
+        repository.put("string", "hello", null);
+        repository.put("integer", 42, null);
+        repository.put("boolean", Boolean.TRUE, null);
+
+        assertThat(repository.get("string")).isEqualTo("hello");
+        assertThat(repository.get("integer")).isEqualTo(42);
+        assertThat(repository.get("boolean")).isEqualTo(Boolean.TRUE);
+    }
+
+    @Test
+    void testMaximumSize() throws Exception {
+        repository.stop();
+
+        CaffeineKeyValueRepository bounded = new CaffeineKeyValueRepository();
+        bounded.setMaximumSize(2);
+        bounded.start();
+
+        try {
+            bounded.put("key1", "value1", null);
+            bounded.put("key2", "value2", null);
+            bounded.put("key3", "value3", null);
+
+            // Caffeine eviction is asynchronous, so wait for it
+            await().atMost(2, TimeUnit.SECONDS)
+                    .untilAsserted(() -> 
assertThat(bounded.size()).isLessThanOrEqualTo(2));
+        } finally {
+            bounded.stop();
+        }
+    }
+
+    @Test
+    void testReplaceMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "value1", "value2", 
null);
+
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testReplaceNonMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "wrong", "value2", null);
+
+        assertThat(replaced).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testReplaceMissingKey() {
+        boolean replaced = repository.replace("nonexistent", "value1", 
"value2", null);
+
+        assertThat(replaced).isFalse();
+    }
+
+    @Test
+    void testReplaceWithTtl() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "value1", "value2", 
Duration.ofMillis(500));
+
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+
+        await().atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(repository.get("key1")).isNull());
+    }
+
+    @Test
+    void testDeleteWithMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean deleted = repository.delete("key1", "value1");
+
+        assertThat(deleted).isTrue();
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteWithNonMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean deleted = repository.delete("key1", "wrong");
+
+        assertThat(deleted).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testDeleteWithMissingKey() {
+        boolean deleted = repository.delete("nonexistent", "value1");
+
+        assertThat(deleted).isFalse();
+    }
+}
diff --git a/components/camel-ehcache/pom.xml b/components/camel-ehcache/pom.xml
index cabf04fbe1d4..8a2d7b0f503f 100644
--- a/components/camel-ehcache/pom.xml
+++ b/components/camel-ehcache/pom.xml
@@ -64,6 +64,17 @@
             <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>${awaitility-version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-test-spring-junit6</artifactId>
diff --git 
a/components/camel-ehcache/src/generated/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryConfigurer.java
 
b/components/camel-ehcache/src/generated/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryConfigurer.java
new file mode 100644
index 000000000000..f78fcc64ff50
--- /dev/null
+++ 
b/components/camel-ehcache/src/generated/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryConfigurer.java
@@ -0,0 +1,57 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.ehcache.processor;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class EhcacheKeyValueRepositoryConfigurer extends 
org.apache.camel.support.component.PropertyConfigurerSupport implements 
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+    @Override
+    public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
+        org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository 
target = 
(org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "cachemanager":
+        case "cacheManager": target.setCacheManager(property(camelContext, 
org.ehcache.CacheManager.class, value)); return true;
+        case "cachename":
+        case "cacheName": target.setCacheName(property(camelContext, 
java.lang.String.class, value)); return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "cachemanager":
+        case "cacheManager": return org.ehcache.CacheManager.class;
+        case "cachename":
+        case "cacheName": return java.lang.String.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository 
target = 
(org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "cachemanager":
+        case "cacheManager": return target.getCacheManager();
+        case "cachename":
+        case "cacheName": return target.getCacheName();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
 
b/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
index 5a1bfa5e46c5..61d3832ae548 100644
--- 
a/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
+++ 
b/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-bean=EhcacheAggregationRepository EhcacheIdempotentRepository
+bean=EhcacheAggregationRepository EhcacheIdempotentRepository 
EhcacheKeyValueRepository
 groupId=org.apache.camel
 artifactId=camel-ehcache
 version=4.23.0-SNAPSHOT
diff --git 
a/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/bean/EhcacheKeyValueRepository.json
 
b/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/bean/EhcacheKeyValueRepository.json
new file mode 100644
index 000000000000..1d137c8f34d5
--- /dev/null
+++ 
b/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/bean/EhcacheKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "EhcacheKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Ehcache Key Value Repository",
+    "description": "An Ehcache-based KeyValueRepository with per-entry TTL 
support.",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-ehcache",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "cacheName": { "index": 0, "kind": "property", 
"displayName": "Cache Name", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": "EhcacheKeyValueRepository", "description": "Name of cache" }, 
"cacheManager": { "index": 1, "kind": "property", "displayName": "Cache 
Manager", "required": false, "type": "object", "javaType": 
"org.ehcache.CacheManager", "deprecated": false, "autowired [...]
+  }
+}
+
diff --git 
a/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository
 
b/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository
new file mode 100644
index 000000000000..2dd8f4b0c1be
--- /dev/null
+++ 
b/components/camel-ehcache/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepository
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.ehcache.processor.EhcacheKeyValueRepositoryConfigurer
diff --git 
a/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepository.java
 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepository.java
new file mode 100644
index 000000000000..35849594320b
--- /dev/null
+++ 
b/components/camel-ehcache/src/main/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepository.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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 org.apache.camel.component.ehcache.processor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.Duration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.component.ehcache.EhcacheManager;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
+import org.ehcache.Cache;
+import org.ehcache.CacheManager;
+
+/**
+ * A {@link KeyValueRepository} implementation backed by an Ehcache {@link 
Cache}.
+ * <p/>
+ * Ehcache does not support per-entry TTL natively (TTL is set at the cache 
configuration level). This implementation
+ * wraps each value in a {@link TtlValue} that records the entry's expiration 
timestamp. Expired entries are removed
+ * lazily on access and during key scans, similar to how {@code 
MemoryKeyValueRepository} handles TTL.
+ * <p/>
+ * This single implementation can serve as idempotent repository, aggregation 
repository, and state store via the
+ * adapters in {@code camel-support} ({@code KeyValueIdempotentRepository} and 
{@code KeyValueAggregationRepository}).
+ *
+ * @since 4.23
+ */
+@Metadata(label = "bean",
+          description = "An Ehcache-based KeyValueRepository with per-entry 
TTL support.",
+          annotations = { 
"interfaceName=org.apache.camel.spi.KeyValueRepository" })
+@Configurer(metadataOnly = true)
+@ManagedResource(description = "Ehcache based key-value repository")
+public class EhcacheKeyValueRepository extends ServiceSupport implements 
KeyValueRepository {
+
+    /**
+     * Internal value wrapper that holds the actual value and an expiration 
timestamp.
+     */
+    static final class TtlValue implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        private final Object value;
+        private final long expiresAt;
+
+        TtlValue(Object value, long expiresAt) {
+            this.value = value;
+            this.expiresAt = expiresAt;
+        }
+
+        Object value() {
+            return value;
+        }
+
+        boolean isExpired() {
+            return System.currentTimeMillis() >= expiresAt;
+        }
+    }
+
+    private Cache<String, TtlValue> cache;
+    private EhcacheManager ehcacheManager;
+
+    @Metadata(description = "Name of cache", defaultValue = 
"EhcacheKeyValueRepository")
+    private String cacheName = EhcacheKeyValueRepository.class.getSimpleName();
+
+    @Metadata(description = "The Ehcache CacheManager to use")
+    private CacheManager cacheManager;
+
+    /**
+     * Creates a new Ehcache-backed key-value repository. A {@link 
CacheManager} must be provided before starting.
+     */
+    public EhcacheKeyValueRepository() {
+    }
+
+    /**
+     * Creates a new Ehcache-backed key-value repository using the given cache 
manager.
+     */
+    public EhcacheKeyValueRepository(CacheManager cacheManager) {
+        this(cacheManager, EhcacheKeyValueRepository.class.getSimpleName());
+    }
+
+    /**
+     * Creates a new Ehcache-backed key-value repository using the given cache 
manager and cache name.
+     */
+    public EhcacheKeyValueRepository(CacheManager cacheManager, String 
cacheName) {
+        this.cacheManager = cacheManager;
+        this.cacheName = cacheName;
+    }
+
+    public void setCacheName(String cacheName) {
+        this.cacheName = cacheName;
+    }
+
+    @ManagedAttribute(description = "The cache name")
+    public String getCacheName() {
+        return cacheName;
+    }
+
+    public CacheManager getCacheManager() {
+        return cacheManager;
+    }
+
+    public void setCacheManager(CacheManager cacheManager) {
+        this.cacheManager = cacheManager;
+    }
+
+    @Override
+    @ManagedOperation(description = "Get value by key")
+    public Object get(String key) {
+        TtlValue entry = cache.get(key);
+        if (entry == null) {
+            return null;
+        }
+        if (entry.isExpired()) {
+            cache.remove(key);
+            return null;
+        }
+        return entry.value();
+    }
+
+    @Override
+    @ManagedOperation(description = "Put a key-value pair with optional TTL")
+    public Object put(String key, Object value, Duration ttl) {
+        long expiresAt = hasPositiveTtl(ttl) ? System.currentTimeMillis() + 
ttl.toMillis() : Long.MAX_VALUE;
+        TtlValue previous = cache.get(key);
+        cache.put(key, new TtlValue(value, expiresAt));
+        if (previous == null || previous.isExpired()) {
+            return null;
+        }
+        return previous.value();
+    }
+
+    @Override
+    @ManagedOperation(description = "Delete a key")
+    public Object delete(String key) {
+        TtlValue entry = cache.get(key);
+        cache.remove(key);
+        if (entry == null || entry.isExpired()) {
+            return null;
+        }
+        return entry.value();
+    }
+
+    @Override
+    @ManagedOperation(description = "Check if key exists")
+    public boolean contains(String key) {
+        TtlValue entry = cache.get(key);
+        if (entry == null) {
+            return false;
+        }
+        if (entry.isExpired()) {
+            cache.remove(key);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public Set<String> keys() {
+        Set<String> keys = new HashSet<>();
+        Iterator<Cache.Entry<String, TtlValue>> it = cache.iterator();
+        while (it.hasNext()) {
+            Cache.Entry<String, TtlValue> entry = it.next();
+            if (!entry.getValue().isExpired()) {
+                keys.add(entry.getKey());
+            } else {
+                cache.remove(entry.getKey());
+            }
+        }
+        return Set.copyOf(keys);
+    }
+
+    @Override
+    @ManagedOperation(description = "Clear all entries")
+    public void clear() {
+        cache.clear();
+    }
+
+    @Override
+    @ManagedAttribute(description = "The number of entries in the repository")
+    public int size() {
+        return keys().size();
+    }
+
+    private static boolean hasPositiveTtl(Duration ttl) {
+        return ttl != null && !ttl.isZero() && !ttl.isNegative();
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        ObjectHelper.notNull(cacheManager, "cacheManager");
+        ehcacheManager = new EhcacheManager(cacheManager, false, null);
+        ehcacheManager.start();
+        cache = ehcacheManager.getCache(cacheName, String.class, 
TtlValue.class);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        if (ehcacheManager != null) {
+            ehcacheManager.stop();
+        }
+    }
+}
diff --git 
a/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryTest.java
 
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryTest.java
new file mode 100644
index 000000000000..ce97b3abb13d
--- /dev/null
+++ 
b/components/camel-ehcache/src/test/java/org/apache/camel/component/ehcache/processor/EhcacheKeyValueRepositoryTest.java
@@ -0,0 +1,269 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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 org.apache.camel.component.ehcache.processor;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.ehcache.CacheManager;
+import org.ehcache.config.builders.CacheConfigurationBuilder;
+import org.ehcache.config.builders.CacheManagerBuilder;
+import org.ehcache.config.builders.ResourcePoolsBuilder;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class EhcacheKeyValueRepositoryTest {
+
+    private static final String CACHE_NAME = "test-kvrepo";
+
+    private CacheManager cacheManager;
+    private EhcacheKeyValueRepository repository;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
+                .withCache(CACHE_NAME,
+                        CacheConfigurationBuilder.newCacheConfigurationBuilder(
+                                String.class,
+                                EhcacheKeyValueRepository.TtlValue.class,
+                                ResourcePoolsBuilder.heap(100)))
+                .build(true);
+
+        repository = new EhcacheKeyValueRepository(cacheManager, CACHE_NAME);
+        repository.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        repository.stop();
+        if (cacheManager != null) {
+            cacheManager.close();
+        }
+    }
+
+    @Test
+    void testPutAndGet() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testGetMissingKeyReturnsNull() {
+        assertThat(repository.get("nonexistent")).isNull();
+    }
+
+    @Test
+    void testPutOverwritesExistingValue() {
+        repository.put("key1", "value1", null);
+        repository.put("key1", "value2", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testPutReturnsPreviousValue() {
+        repository.put("key1", "value1", null);
+        Object previous = repository.put("key1", "value2", null);
+
+        assertThat(previous).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutReturnsNullForNewKey() {
+        Object previous = repository.put("key1", "value1", null);
+
+        assertThat(previous).isNull();
+    }
+
+    @Test
+    void testDelete() {
+        repository.put("key1", "value1", null);
+
+        Object deleted = repository.delete("key1");
+
+        assertThat(deleted).isEqualTo("value1");
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteMissingKeyReturnsNull() {
+        assertThat(repository.delete("nonexistent")).isNull();
+    }
+
+    @Test
+    void testContains() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.contains("key1")).isTrue();
+        assertThat(repository.contains("nonexistent")).isFalse();
+    }
+
+    @Test
+    void testKeys() {
+        repository.put("key1", "value1", null);
+        repository.put("key2", "value2", null);
+        repository.put("key3", "value3", null);
+
+        Set<String> keys = repository.keys();
+
+        assertThat(keys).containsExactlyInAnyOrder("key1", "key2", "key3");
+    }
+
+    @Test
+    void testKeysEmpty() {
+        assertThat(repository.keys()).isEmpty();
+    }
+
+    @Test
+    void testClear() {
+        repository.put("key1", "value1", null);
+        repository.put("key2", "value2", null);
+
+        repository.clear();
+
+        assertThat(repository.size()).isZero();
+        assertThat(repository.get("key1")).isNull();
+        assertThat(repository.get("key2")).isNull();
+    }
+
+    @Test
+    void testSize() {
+        assertThat(repository.size()).isZero();
+
+        repository.put("key1", "value1", null);
+        assertThat(repository.size()).isEqualTo(1);
+
+        repository.put("key2", "value2", null);
+        assertThat(repository.size()).isEqualTo(2);
+
+        repository.delete("key1");
+        assertThat(repository.size()).isEqualTo(1);
+    }
+
+    @Test
+    void testTtlExpiration() {
+        repository.put("key1", "value1", Duration.ofMillis(100));
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+
+        await().atMost(2, TimeUnit.SECONDS)
+                .untilAsserted(() -> {
+                    assertThat(repository.get("key1")).isNull();
+                    assertThat(repository.contains("key1")).isFalse();
+                });
+    }
+
+    @Test
+    void testTtlExpirationOnKeys() {
+        repository.put("key1", "value1", Duration.ofMillis(100));
+        repository.put("key2", "value2", null); // no expiration
+
+        await().atMost(2, TimeUnit.SECONDS)
+                .untilAsserted(() -> {
+                    assertThat(repository.keys()).containsExactly("key2");
+                    assertThat(repository.size()).isEqualTo(1);
+                });
+    }
+
+    @Test
+    void testNoTtlWithZero() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+    }
+
+    @Test
+    void testNoTtlWithNegative() {
+        repository.put("key1", "value1", Duration.ofMillis(-1));
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+    }
+
+    @Test
+    void testStoresDifferentValueTypes() {
+        repository.put("string", "hello", null);
+        repository.put("integer", 42, null);
+        repository.put("boolean", Boolean.TRUE, null);
+
+        assertThat(repository.get("string")).isEqualTo("hello");
+        assertThat(repository.get("integer")).isEqualTo(42);
+        assertThat(repository.get("boolean")).isEqualTo(Boolean.TRUE);
+    }
+
+    @Test
+    void testReplaceMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "value1", "value2", 
null);
+
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testReplaceNonMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "wrong", "value2", null);
+
+        assertThat(replaced).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testReplaceMissingKey() {
+        boolean replaced = repository.replace("nonexistent", "value1", 
"value2", null);
+
+        assertThat(replaced).isFalse();
+    }
+
+    @Test
+    void testDeleteWithMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean deleted = repository.delete("key1", "value1");
+
+        assertThat(deleted).isTrue();
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteWithNonMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean deleted = repository.delete("key1", "wrong");
+
+        assertThat(deleted).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testDeleteWithMissingKey() {
+        boolean deleted = repository.delete("nonexistent", "value1");
+
+        assertThat(deleted).isFalse();
+    }
+}
diff --git a/components/camel-hazelcast/pom.xml 
b/components/camel-hazelcast/pom.xml
index 341bcb071cfd..4c50ffb9ea34 100644
--- a/components/camel-hazelcast/pom.xml
+++ b/components/camel-hazelcast/pom.xml
@@ -84,6 +84,12 @@
             <artifactId>assertj-core</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>${awaitility-version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <profiles>
diff --git 
a/components/camel-hazelcast/src/generated/java/org/apache/camel/component/hazelcast/HazelcastKeyValueRepositoryConfigurer.java
 
b/components/camel-hazelcast/src/generated/java/org/apache/camel/component/hazelcast/HazelcastKeyValueRepositoryConfigurer.java
new file mode 100644
index 000000000000..d2b8190ec5d7
--- /dev/null
+++ 
b/components/camel-hazelcast/src/generated/java/org/apache/camel/component/hazelcast/HazelcastKeyValueRepositoryConfigurer.java
@@ -0,0 +1,57 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.hazelcast;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.component.hazelcast.HazelcastKeyValueRepository;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class HazelcastKeyValueRepositoryConfigurer extends 
org.apache.camel.support.component.PropertyConfigurerSupport implements 
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+    @Override
+    public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
+        org.apache.camel.component.hazelcast.HazelcastKeyValueRepository 
target = (org.apache.camel.component.hazelcast.HazelcastKeyValueRepository) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "hazelcastinstance":
+        case "hazelcastInstance": 
target.setHazelcastInstance(property(camelContext, 
com.hazelcast.core.HazelcastInstance.class, value)); return true;
+        case "mapname":
+        case "mapName": target.setMapName(property(camelContext, 
java.lang.String.class, value)); return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "hazelcastinstance":
+        case "hazelcastInstance": return 
com.hazelcast.core.HazelcastInstance.class;
+        case "mapname":
+        case "mapName": return java.lang.String.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        org.apache.camel.component.hazelcast.HazelcastKeyValueRepository 
target = (org.apache.camel.component.hazelcast.HazelcastKeyValueRepository) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "hazelcastinstance":
+        case "hazelcastInstance": return target.getHazelcastInstance();
+        case "mapname":
+        case "mapName": return target.getMapName();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
 
b/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
index 8c6cfb5558bd..2a367586072a 100644
--- 
a/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
+++ 
b/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-bean=HazelcastAggregationRepository HazelcastIdempotentRepository
+bean=HazelcastAggregationRepository HazelcastIdempotentRepository 
HazelcastKeyValueRepository
 groupId=org.apache.camel
 artifactId=camel-hazelcast
 version=4.23.0-SNAPSHOT
diff --git 
a/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/bean/HazelcastKeyValueRepository.json
 
b/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/bean/HazelcastKeyValueRepository.json
new file mode 100644
index 000000000000..22d363a3f59c
--- /dev/null
+++ 
b/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/bean/HazelcastKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "HazelcastKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.hazelcast.HazelcastKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "Hazelcast Key Value Repository",
+    "description": "A Hazelcast-based KeyValueRepository with native per-entry 
TTL support.",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-hazelcast",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "mapName": { "index": 0, "kind": "property", 
"displayName": "Map Name", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": "HazelcastKeyValueRepository", "description": "Name of the 
Hazelcast map to use" }, "hazelcastInstance": { "index": 1, "kind": "property", 
"displayName": "Hazelcast Instance", "required": false, "type": "object", 
"javaType": "com.hazelcast.core.HazelcastIns [...]
+  }
+}
+
diff --git 
a/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.hazelcast.HazelcastKeyValueRepository
 
b/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.hazelcast.HazelcastKeyValueRepository
new file mode 100644
index 000000000000..aead6a891a1b
--- /dev/null
+++ 
b/components/camel-hazelcast/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.hazelcast.HazelcastKeyValueRepository
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.hazelcast.HazelcastKeyValueRepositoryConfigurer
diff --git 
a/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/HazelcastKeyValueRepository.java
 
b/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/HazelcastKeyValueRepository.java
new file mode 100644
index 000000000000..fd8df970d4da
--- /dev/null
+++ 
b/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/HazelcastKeyValueRepository.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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 org.apache.camel.component.hazelcast;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import com.hazelcast.config.Config;
+import com.hazelcast.config.XmlConfigBuilder;
+import com.hazelcast.core.Hazelcast;
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.map.IMap;
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.KeyValueRepositoryHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * A {@link KeyValueRepository} implementation backed by a Hazelcast {@link 
IMap} with native per-entry TTL support.
+ * <p/>
+ * Hazelcast natively supports per-entry TTL via {@link IMap#put(Object, 
Object, long, TimeUnit)} and
+ * {@link IMap#putIfAbsent(Object, Object, long, TimeUnit)}, making it a 
natural fit for the {@code KeyValueRepository}
+ * contract without requiring wrapper objects or lazy eviction.
+ * <p/>
+ * Values are serialized through {@link KeyValueRepositoryHelper} (plain Java 
serialization) and stored as raw
+ * {@code byte[]} in the Hazelcast map. This ensures a consistent 
serialization format across all persistent
+ * {@link KeyValueRepository} implementations and avoids coupling to 
Hazelcast's built-in serialization.
+ * <p/>
+ * This single implementation can serve as idempotent repository, aggregation 
repository, and state store via the
+ * adapters in {@code camel-support} ({@code KeyValueIdempotentRepository} and 
{@code KeyValueAggregationRepository}).
+ * <p/>
+ * If no external {@link HazelcastInstance} is provided, a local instance is 
created on start and shut down on stop.
+ *
+ * @since 4.23
+ */
+@Metadata(label = "bean",
+          description = "A Hazelcast-based KeyValueRepository with native 
per-entry TTL support.",
+          annotations = { 
"interfaceName=org.apache.camel.spi.KeyValueRepository" })
+@Configurer(metadataOnly = true)
+@ManagedResource(description = "Hazelcast based key-value repository")
+public class HazelcastKeyValueRepository extends ServiceSupport implements 
KeyValueRepository {
+
+    private boolean useLocalHzInstance;
+    private IMap<String, byte[]> map;
+
+    @Metadata(description = "Name of the Hazelcast map to use", defaultValue = 
"HazelcastKeyValueRepository")
+    private String mapName = HazelcastKeyValueRepository.class.getSimpleName();
+
+    @Metadata(description = "To use an existing Hazelcast instance instead of 
creating a local one")
+    private HazelcastInstance hazelcastInstance;
+
+    /**
+     * Creates a new Hazelcast-backed key-value repository with default 
settings (local instance, default map name).
+     */
+    public HazelcastKeyValueRepository() {
+    }
+
+    /**
+     * Creates a new Hazelcast-backed key-value repository using the given 
Hazelcast instance.
+     */
+    public HazelcastKeyValueRepository(HazelcastInstance hazelcastInstance) {
+        this(hazelcastInstance, 
HazelcastKeyValueRepository.class.getSimpleName());
+    }
+
+    /**
+     * Creates a new Hazelcast-backed key-value repository using the given 
Hazelcast instance and map name.
+     */
+    public HazelcastKeyValueRepository(HazelcastInstance hazelcastInstance, 
String mapName) {
+        this.hazelcastInstance = hazelcastInstance;
+        this.mapName = mapName;
+    }
+
+    public void setMapName(String mapName) {
+        this.mapName = mapName;
+    }
+
+    @ManagedAttribute(description = "The Hazelcast map name")
+    public String getMapName() {
+        return mapName;
+    }
+
+    public HazelcastInstance getHazelcastInstance() {
+        return hazelcastInstance;
+    }
+
+    public void setHazelcastInstance(HazelcastInstance hazelcastInstance) {
+        this.hazelcastInstance = hazelcastInstance;
+    }
+
+    @Override
+    @ManagedOperation(description = "Get value by key")
+    public Object get(String key) {
+        byte[] bytes = map.get(key);
+        return bytes != null ? KeyValueRepositoryHelper.deserialize(bytes) : 
null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Put a key-value pair with optional TTL")
+    public Object put(String key, Object value, Duration ttl) {
+        byte[] serialized = KeyValueRepositoryHelper.serialize(value);
+        byte[] previous;
+        if (hasPositiveTtl(ttl)) {
+            previous = map.put(key, serialized, ttl.toMillis(), 
TimeUnit.MILLISECONDS);
+        } else {
+            previous = map.put(key, serialized);
+        }
+        return previous != null ? 
KeyValueRepositoryHelper.deserialize(previous) : null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Delete a key")
+    public Object delete(String key) {
+        byte[] bytes = map.remove(key);
+        return bytes != null ? KeyValueRepositoryHelper.deserialize(bytes) : 
null;
+    }
+
+    @Override
+    @ManagedOperation(description = "Check if key exists")
+    public boolean contains(String key) {
+        return map.containsKey(key);
+    }
+
+    @Override
+    public Set<String> keys() {
+        return Collections.unmodifiableSet(map.keySet());
+    }
+
+    @Override
+    @ManagedOperation(description = "Clear all entries")
+    public void clear() {
+        map.clear();
+    }
+
+    @Override
+    public Object putIfAbsent(String key, Object value, Duration ttl) {
+        byte[] serialized = KeyValueRepositoryHelper.serialize(value);
+        byte[] existing;
+        if (hasPositiveTtl(ttl)) {
+            existing = map.putIfAbsent(key, serialized, ttl.toMillis(), 
TimeUnit.MILLISECONDS);
+        } else {
+            existing = map.putIfAbsent(key, serialized);
+        }
+        return existing != null ? 
KeyValueRepositoryHelper.deserialize(existing) : null;
+    }
+
+    @Override
+    public boolean replace(String key, Object expectedOldValue, Object 
newValue, Duration ttl) {
+        byte[] expectedBytes = 
KeyValueRepositoryHelper.serialize(expectedOldValue);
+        byte[] newBytes = KeyValueRepositoryHelper.serialize(newValue);
+        if (!hasPositiveTtl(ttl)) {
+            return map.replace(key, expectedBytes, newBytes);
+        }
+        // IMap.replace(K, V, V) does not support TTL, so use distributed lock 
for atomicity
+        map.lock(key);
+        try {
+            byte[] current = map.get(key);
+            if (!Arrays.equals(current, expectedBytes)) {
+                return false;
+            }
+            map.put(key, newBytes, ttl.toMillis(), TimeUnit.MILLISECONDS);
+            return true;
+        } finally {
+            map.unlock(key);
+        }
+    }
+
+    @Override
+    public boolean delete(String key, Object expectedValue) {
+        byte[] expectedBytes = 
KeyValueRepositoryHelper.serialize(expectedValue);
+        return map.remove(key, expectedBytes);
+    }
+
+    @Override
+    @ManagedAttribute(description = "The number of entries in the repository")
+    public int size() {
+        return map.size();
+    }
+
+    private static boolean hasPositiveTtl(Duration ttl) {
+        return ttl != null && !ttl.isZero() && !ttl.isNegative();
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        if (hazelcastInstance == null) {
+            Config cfg = new XmlConfigBuilder().build();
+            cfg.setProperty("hazelcast.version.check.enabled", "false");
+            HazelcastSerializationFilterHelper.applyDefault(cfg);
+            hazelcastInstance = Hazelcast.newHazelcastInstance(cfg);
+            useLocalHzInstance = true;
+        } else {
+            ObjectHelper.notNull(hazelcastInstance, "hazelcastInstance");
+        }
+        map = hazelcastInstance.getMap(mapName);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        if (useLocalHzInstance && hazelcastInstance != null) {
+            hazelcastInstance.getLifecycleService().shutdown();
+            hazelcastInstance = null;
+        }
+    }
+}
diff --git 
a/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastKeyValueRepositoryTest.java
 
b/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastKeyValueRepositoryTest.java
new file mode 100644
index 000000000000..e69f606b3f59
--- /dev/null
+++ 
b/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastKeyValueRepositoryTest.java
@@ -0,0 +1,300 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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 org.apache.camel.component.hazelcast;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import com.hazelcast.config.Config;
+import com.hazelcast.core.Hazelcast;
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.map.IMap;
+import org.apache.camel.test.infra.hazelcast.services.HazelcastService;
+import org.apache.camel.test.infra.hazelcast.services.HazelcastServiceFactory;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class HazelcastKeyValueRepositoryTest {
+
+    @RegisterExtension
+    static HazelcastService hazelcastService = 
HazelcastServiceFactory.createSingletonService();
+
+    private static final String MAP_NAME = "kvrepo-test";
+
+    private HazelcastInstance hazelcastInstance;
+    private IMap<String, Object> map;
+    private HazelcastKeyValueRepository repository;
+
+    @BeforeAll
+    void setupHazelcast() throws Exception {
+        Config config = hazelcastService.createConfiguration(null, 0, null, 
MAP_NAME);
+        hazelcastInstance = Hazelcast.newHazelcastInstance(config);
+        repository = new HazelcastKeyValueRepository(hazelcastInstance, 
MAP_NAME);
+        repository.start();
+        map = hazelcastInstance.getMap(MAP_NAME);
+    }
+
+    @AfterAll
+    void teardownHazelcast() throws Exception {
+        if (repository != null) {
+            repository.stop();
+        }
+        if (hazelcastInstance != null) {
+            hazelcastInstance.getLifecycleService().terminate();
+        }
+    }
+
+    @BeforeEach
+    void clearMap() {
+        map.clear();
+    }
+
+    @Test
+    void testPutAndGet() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testGetMissingKeyReturnsNull() {
+        assertThat(repository.get("nonexistent")).isNull();
+    }
+
+    @Test
+    void testPutOverwritesExistingValue() {
+        repository.put("key1", "value1", null);
+        repository.put("key1", "value2", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testPutReturnsPreviousValue() {
+        repository.put("key1", "value1", null);
+        Object previous = repository.put("key1", "value2", null);
+
+        assertThat(previous).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutReturnsNullForNewKey() {
+        Object previous = repository.put("key1", "value1", null);
+
+        assertThat(previous).isNull();
+    }
+
+    @Test
+    void testDelete() {
+        repository.put("key1", "value1", null);
+
+        Object deleted = repository.delete("key1");
+
+        assertThat(deleted).isEqualTo("value1");
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteMissingKeyReturnsNull() {
+        assertThat(repository.delete("nonexistent")).isNull();
+    }
+
+    @Test
+    void testContains() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.contains("key1")).isTrue();
+        assertThat(repository.contains("nonexistent")).isFalse();
+    }
+
+    @Test
+    void testKeys() {
+        repository.put("key1", "value1", null);
+        repository.put("key2", "value2", null);
+        repository.put("key3", "value3", null);
+
+        Set<String> keys = repository.keys();
+
+        assertThat(keys).containsExactlyInAnyOrder("key1", "key2", "key3");
+    }
+
+    @Test
+    void testKeysEmpty() {
+        assertThat(repository.keys()).isEmpty();
+    }
+
+    @Test
+    void testClear() {
+        repository.put("key1", "value1", null);
+        repository.put("key2", "value2", null);
+
+        repository.clear();
+
+        assertThat(repository.size()).isZero();
+        assertThat(repository.get("key1")).isNull();
+        assertThat(repository.get("key2")).isNull();
+    }
+
+    @Test
+    void testSize() {
+        assertThat(repository.size()).isZero();
+
+        repository.put("key1", "value1", null);
+        assertThat(repository.size()).isEqualTo(1);
+
+        repository.put("key2", "value2", null);
+        assertThat(repository.size()).isEqualTo(2);
+
+        repository.delete("key1");
+        assertThat(repository.size()).isEqualTo(1);
+    }
+
+    @Test
+    void testPutIfAbsentNewKey() {
+        Object result = repository.putIfAbsent("key1", "value1", null);
+
+        assertThat(result).isNull();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutIfAbsentExistingKey() {
+        repository.put("key1", "value1", null);
+
+        Object result = repository.putIfAbsent("key1", "value2", null);
+
+        assertThat(result).isEqualTo("value1");
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testTtlExpiration() {
+        repository.put("key1", "value1", Duration.ofMillis(2000));
+
+        assertThat(repository.contains("key1")).isTrue();
+
+        // Hazelcast TTL uses its own eviction mechanism
+        await().atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> {
+                    assertThat(repository.get("key1")).isNull();
+                    assertThat(repository.contains("key1")).isFalse();
+                });
+    }
+
+    @Test
+    void testNoTtlWithZero() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+    }
+
+    @Test
+    void testNoTtlWithNegative() {
+        repository.put("key1", "value1", Duration.ofMillis(-1));
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+    }
+
+    @Test
+    void testStoresDifferentValueTypes() {
+        repository.put("string", "hello", null);
+        repository.put("integer", 42, null);
+        repository.put("boolean", Boolean.TRUE, null);
+
+        assertThat(repository.get("string")).isEqualTo("hello");
+        assertThat(repository.get("integer")).isEqualTo(42);
+        assertThat(repository.get("boolean")).isEqualTo(Boolean.TRUE);
+    }
+
+    @Test
+    void testReplaceMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "value1", "value2", 
null);
+
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testReplaceNonMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "wrong", "value2", null);
+
+        assertThat(replaced).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testReplaceMissingKey() {
+        boolean replaced = repository.replace("nonexistent", "value1", 
"value2", null);
+
+        assertThat(replaced).isFalse();
+    }
+
+    @Test
+    void testReplaceWithTtl() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "value1", "value2", 
Duration.ofMillis(2000));
+
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+
+        await().atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertThat(repository.get("key1")).isNull());
+    }
+
+    @Test
+    void testDeleteWithMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean deleted = repository.delete("key1", "value1");
+
+        assertThat(deleted).isTrue();
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteWithNonMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean deleted = repository.delete("key1", "wrong");
+
+        assertThat(deleted).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testDeleteWithMissingKey() {
+        boolean deleted = repository.delete("nonexistent", "value1");
+
+        assertThat(deleted).isFalse();
+    }
+}
diff --git a/components/camel-jcache/pom.xml b/components/camel-jcache/pom.xml
index 352c816338af..dcfd131a9d89 100644
--- a/components/camel-jcache/pom.xml
+++ b/components/camel-jcache/pom.xml
@@ -53,6 +53,17 @@
             <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>${awaitility-version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-test-spring-junit6</artifactId>
diff --git 
a/components/camel-jcache/src/generated/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryConfigurer.java
 
b/components/camel-jcache/src/generated/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryConfigurer.java
new file mode 100644
index 000000000000..f62511caec00
--- /dev/null
+++ 
b/components/camel-jcache/src/generated/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryConfigurer.java
@@ -0,0 +1,54 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.jcache.processor;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.component.jcache.processor.JCacheKeyValueRepository;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class JCacheKeyValueRepositoryConfigurer extends 
org.apache.camel.support.component.PropertyConfigurerSupport implements 
GeneratedPropertyConfigurer, PropertyConfigurerGetter {
+
+    @Override
+    public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
+        org.apache.camel.component.jcache.processor.JCacheKeyValueRepository 
target = (org.apache.camel.component.jcache.processor.JCacheKeyValueRepository) 
obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "cachename":
+        case "cacheName": target.setCacheName(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "configuration": target.setConfiguration(property(camelContext, 
org.apache.camel.component.jcache.JCacheConfiguration.class, value)); return 
true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "cachename":
+        case "cacheName": return java.lang.String.class;
+        case "configuration": return 
org.apache.camel.component.jcache.JCacheConfiguration.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        org.apache.camel.component.jcache.processor.JCacheKeyValueRepository 
target = (org.apache.camel.component.jcache.processor.JCacheKeyValueRepository) 
obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "cachename":
+        case "cacheName": return target.getCacheName();
+        case "configuration": return target.getConfiguration();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
 
b/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
index 0f17c694e720..a065140f1885 100644
--- 
a/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
+++ 
b/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/bean.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-bean=JCacheAggregationRepository JCacheIdempotentRepository
+bean=JCacheAggregationRepository JCacheIdempotentRepository 
JCacheKeyValueRepository
 groupId=org.apache.camel
 artifactId=camel-jcache
 version=4.23.0-SNAPSHOT
diff --git 
a/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/bean/JCacheKeyValueRepository.json
 
b/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/bean/JCacheKeyValueRepository.json
new file mode 100644
index 000000000000..3150cfb87a19
--- /dev/null
+++ 
b/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/bean/JCacheKeyValueRepository.json
@@ -0,0 +1,16 @@
+{
+  "bean": {
+    "kind": "bean",
+    "name": "JCacheKeyValueRepository",
+    "javaType": 
"org.apache.camel.component.jcache.processor.JCacheKeyValueRepository",
+    "interfaceType": "org.apache.camel.spi.KeyValueRepository",
+    "title": "JCache Key Value Repository",
+    "description": "A JCache-based KeyValueRepository with per-entry TTL 
support.",
+    "deprecated": false,
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-jcache",
+    "version": "4.23.0-SNAPSHOT",
+    "properties": { "configuration": { "index": 0, "kind": "property", 
"displayName": "Configuration", "required": false, "type": "object", 
"javaType": "org.apache.camel.component.jcache.JCacheConfiguration", 
"deprecated": false, "autowired": false, "secret": false, "description": 
"Configuration for JCache" } }
+  }
+}
+
diff --git 
a/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.jcache.processor.JCacheKeyValueRepository
 
b/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.jcache.processor.JCacheKeyValueRepository
new file mode 100644
index 000000000000..98e42d3639b4
--- /dev/null
+++ 
b/components/camel-jcache/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.component.jcache.processor.JCacheKeyValueRepository
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.component.jcache.processor.JCacheKeyValueRepositoryConfigurer
diff --git 
a/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepository.java
 
b/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepository.java
new file mode 100644
index 000000000000..85bf205f329a
--- /dev/null
+++ 
b/components/camel-jcache/src/main/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepository.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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 org.apache.camel.component.jcache.processor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.Duration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.cache.Cache;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.component.jcache.JCacheConfiguration;
+import org.apache.camel.component.jcache.JCacheHelper;
+import org.apache.camel.component.jcache.JCacheManager;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.KeyValueRepository;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * A {@link KeyValueRepository} implementation backed by a JCache (JSR-107) 
{@link Cache}.
+ * <p/>
+ * JCache does not support per-entry TTL natively (expiry is configured at the 
cache level via
+ * {@link javax.cache.expiry.ExpiryPolicy}). This implementation wraps each 
value in a {@link TtlValue} that records the
+ * entry's expiration timestamp. Expired entries are removed lazily on access 
and during key scans.
+ * <p/>
+ * This single implementation can serve as idempotent repository, aggregation 
repository, and state store via the
+ * adapters in {@code camel-support} ({@code KeyValueIdempotentRepository} and 
{@code KeyValueAggregationRepository}).
+ *
+ * @since 4.23
+ */
+@Metadata(label = "bean",
+          description = "A JCache-based KeyValueRepository with per-entry TTL 
support.",
+          annotations = { 
"interfaceName=org.apache.camel.spi.KeyValueRepository" })
+@Configurer(metadataOnly = true)
+@ManagedResource(description = "JCache based key-value repository")
+public class JCacheKeyValueRepository extends ServiceSupport implements 
CamelContextAware, KeyValueRepository {
+
+    /**
+     * Internal value wrapper that holds the actual value and an expiration 
timestamp.
+     */
+    static final class TtlValue implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        private final Object value;
+        private final long expiresAt;
+
+        TtlValue(Object value, long expiresAt) {
+            this.value = value;
+            this.expiresAt = expiresAt;
+        }
+
+        Object value() {
+            return value;
+        }
+
+        boolean isExpired() {
+            return System.currentTimeMillis() >= expiresAt;
+        }
+    }
+
+    private CamelContext camelContext;
+    private Cache<String, TtlValue> cache;
+    private JCacheManager<String, TtlValue> cacheManager;
+
+    @Metadata(description = "Configuration for JCache")
+    private JCacheConfiguration configuration;
+
+    /**
+     * Creates a new JCache-backed key-value repository with a default 
configuration.
+     */
+    public JCacheKeyValueRepository() {
+        this.configuration = new JCacheConfiguration();
+    }
+
+    @Override
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        this.camelContext = camelContext;
+    }
+
+    public JCacheConfiguration getConfiguration() {
+        return configuration;
+    }
+
+    public void setConfiguration(JCacheConfiguration configuration) {
+        this.configuration = configuration;
+    }
+
+    public Cache<String, TtlValue> getCache() {
+        return cache;
+    }
+
+    public void setCache(Cache<String, TtlValue> cache) {
+        this.cache = cache;
+    }
+
+    public void setCacheName(String cacheName) {
+        configuration.setCacheName(cacheName);
+    }
+
+    @ManagedAttribute(description = "The cache name")
+    public String getCacheName() {
+        return configuration.getCacheName();
+    }
+
+    @Override
+    @ManagedOperation(description = "Get value by key")
+    public Object get(String key) {
+        TtlValue entry = cache.get(key);
+        if (entry == null) {
+            return null;
+        }
+        if (entry.isExpired()) {
+            cache.remove(key);
+            return null;
+        }
+        return entry.value();
+    }
+
+    @Override
+    @ManagedOperation(description = "Put a key-value pair with optional TTL")
+    public Object put(String key, Object value, Duration ttl) {
+        long expiresAt = hasPositiveTtl(ttl) ? System.currentTimeMillis() + 
ttl.toMillis() : Long.MAX_VALUE;
+        TtlValue previous = cache.get(key);
+        cache.put(key, new TtlValue(value, expiresAt));
+        if (previous == null || previous.isExpired()) {
+            return null;
+        }
+        return previous.value();
+    }
+
+    @Override
+    @ManagedOperation(description = "Delete a key")
+    public Object delete(String key) {
+        TtlValue entry = cache.get(key);
+        cache.remove(key);
+        if (entry == null || entry.isExpired()) {
+            return null;
+        }
+        return entry.value();
+    }
+
+    @Override
+    @ManagedOperation(description = "Check if key exists")
+    public boolean contains(String key) {
+        TtlValue entry = cache.get(key);
+        if (entry == null) {
+            return false;
+        }
+        if (entry.isExpired()) {
+            cache.remove(key);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public Set<String> keys() {
+        Set<String> keys = new HashSet<>();
+        Iterator<Cache.Entry<String, TtlValue>> it = cache.iterator();
+        while (it.hasNext()) {
+            Cache.Entry<String, TtlValue> entry = it.next();
+            if (!entry.getValue().isExpired()) {
+                keys.add(entry.getKey());
+            } else {
+                cache.remove(entry.getKey());
+            }
+        }
+        return Set.copyOf(keys);
+    }
+
+    @Override
+    @ManagedOperation(description = "Clear all entries")
+    public void clear() {
+        cache.clear();
+    }
+
+    @Override
+    @ManagedAttribute(description = "The number of entries in the repository")
+    public int size() {
+        return keys().size();
+    }
+
+    private static boolean hasPositiveTtl(Duration ttl) {
+        return ttl != null && !ttl.isZero() && !ttl.isNegative();
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        ObjectHelper.notNull(camelContext, "camelContext");
+        ObjectHelper.notNull(configuration, "configuration");
+
+        if (cache != null) {
+            cacheManager = new JCacheManager<>(cache);
+        } else {
+            cacheManager = JCacheHelper.createManager(getCamelContext(), 
configuration);
+            cache = cacheManager.getCache();
+        }
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        if (cacheManager != null) {
+            cacheManager.close();
+        }
+    }
+}
diff --git 
a/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryTest.java
 
b/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryTest.java
new file mode 100644
index 000000000000..f7077c08eb69
--- /dev/null
+++ 
b/components/camel-jcache/src/test/java/org/apache/camel/component/jcache/processor/JCacheKeyValueRepositoryTest.java
@@ -0,0 +1,269 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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 org.apache.camel.component.jcache.processor;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import javax.cache.Cache;
+
+import org.apache.camel.component.jcache.JCacheConfiguration;
+import org.apache.camel.component.jcache.JCacheHelper;
+import org.apache.camel.component.jcache.JCacheManager;
+import org.apache.camel.component.jcache.support.HazelcastTest;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+@HazelcastTest
+class JCacheKeyValueRepositoryTest extends CamelTestSupport {
+
+    private JCacheManager<String, JCacheKeyValueRepository.TtlValue> 
cacheManager;
+    private Cache<String, JCacheKeyValueRepository.TtlValue> cache;
+    private JCacheKeyValueRepository repository;
+
+    @Override
+    public void doPostSetup() throws Exception {
+        cacheManager = JCacheHelper.createManager(context, new 
JCacheConfiguration("kvrepo-test"));
+        cache = cacheManager.getCache();
+
+        repository = new JCacheKeyValueRepository();
+        repository.setCamelContext(context);
+        repository.setCache(cache);
+        repository.start();
+    }
+
+    @Override
+    public void doPostTearDown() throws Exception {
+        if (repository != null) {
+            repository.stop();
+        }
+        if (cacheManager != null) {
+            cacheManager.close();
+        }
+    }
+
+    @Test
+    void testPutAndGet() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testGetMissingKeyReturnsNull() {
+        assertThat(repository.get("nonexistent")).isNull();
+    }
+
+    @Test
+    void testPutOverwritesExistingValue() {
+        repository.put("key1", "value1", null);
+        repository.put("key1", "value2", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testPutReturnsPreviousValue() {
+        repository.put("key1", "value1", null);
+        Object previous = repository.put("key1", "value2", null);
+
+        assertThat(previous).isEqualTo("value1");
+    }
+
+    @Test
+    void testPutReturnsNullForNewKey() {
+        Object previous = repository.put("key1", "value1", null);
+
+        assertThat(previous).isNull();
+    }
+
+    @Test
+    void testDelete() {
+        repository.put("key1", "value1", null);
+
+        Object deleted = repository.delete("key1");
+
+        assertThat(deleted).isEqualTo("value1");
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteMissingKeyReturnsNull() {
+        assertThat(repository.delete("nonexistent")).isNull();
+    }
+
+    @Test
+    void testContains() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.contains("key1")).isTrue();
+        assertThat(repository.contains("nonexistent")).isFalse();
+    }
+
+    @Test
+    void testKeys() {
+        repository.put("key1", "value1", null);
+        repository.put("key2", "value2", null);
+        repository.put("key3", "value3", null);
+
+        Set<String> keys = repository.keys();
+
+        assertThat(keys).containsExactlyInAnyOrder("key1", "key2", "key3");
+    }
+
+    @Test
+    void testKeysEmpty() {
+        assertThat(repository.keys()).isEmpty();
+    }
+
+    @Test
+    void testClear() {
+        repository.put("key1", "value1", null);
+        repository.put("key2", "value2", null);
+
+        repository.clear();
+
+        assertThat(repository.size()).isZero();
+        assertThat(repository.get("key1")).isNull();
+        assertThat(repository.get("key2")).isNull();
+    }
+
+    @Test
+    void testSize() {
+        assertThat(repository.size()).isZero();
+
+        repository.put("key1", "value1", null);
+        assertThat(repository.size()).isEqualTo(1);
+
+        repository.put("key2", "value2", null);
+        assertThat(repository.size()).isEqualTo(2);
+
+        repository.delete("key1");
+        assertThat(repository.size()).isEqualTo(1);
+    }
+
+    @Test
+    void testTtlExpiration() {
+        repository.put("key1", "value1", Duration.ofMillis(100));
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+
+        await().atMost(2, TimeUnit.SECONDS)
+                .untilAsserted(() -> {
+                    assertThat(repository.get("key1")).isNull();
+                    assertThat(repository.contains("key1")).isFalse();
+                });
+    }
+
+    @Test
+    void testTtlExpirationOnKeys() {
+        repository.put("key1", "value1", Duration.ofMillis(100));
+        repository.put("key2", "value2", null); // no expiration
+
+        await().atMost(2, TimeUnit.SECONDS)
+                .untilAsserted(() -> {
+                    assertThat(repository.keys()).containsExactly("key2");
+                    assertThat(repository.size()).isEqualTo(1);
+                });
+    }
+
+    @Test
+    void testNoTtlWithZero() {
+        repository.put("key1", "value1", null);
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+    }
+
+    @Test
+    void testNoTtlWithNegative() {
+        repository.put("key1", "value1", Duration.ofMillis(-1));
+
+        assertThat(repository.get("key1")).isEqualTo("value1");
+        assertThat(repository.contains("key1")).isTrue();
+    }
+
+    @Test
+    void testStoresDifferentValueTypes() {
+        repository.put("string", "hello", null);
+        repository.put("integer", 42, null);
+        repository.put("boolean", Boolean.TRUE, null);
+
+        assertThat(repository.get("string")).isEqualTo("hello");
+        assertThat(repository.get("integer")).isEqualTo(42);
+        assertThat(repository.get("boolean")).isEqualTo(Boolean.TRUE);
+    }
+
+    @Test
+    void testReplaceMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "value1", "value2", 
null);
+
+        assertThat(replaced).isTrue();
+        assertThat(repository.get("key1")).isEqualTo("value2");
+    }
+
+    @Test
+    void testReplaceNonMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean replaced = repository.replace("key1", "wrong", "value2", null);
+
+        assertThat(replaced).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testReplaceMissingKey() {
+        boolean replaced = repository.replace("nonexistent", "value1", 
"value2", null);
+
+        assertThat(replaced).isFalse();
+    }
+
+    @Test
+    void testDeleteWithMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean deleted = repository.delete("key1", "value1");
+
+        assertThat(deleted).isTrue();
+        assertThat(repository.get("key1")).isNull();
+    }
+
+    @Test
+    void testDeleteWithNonMatchingValue() {
+        repository.put("key1", "value1", null);
+
+        boolean deleted = repository.delete("key1", "wrong");
+
+        assertThat(deleted).isFalse();
+        assertThat(repository.get("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    void testDeleteWithMissingKey() {
+        boolean deleted = repository.delete("nonexistent", "value1");
+
+        assertThat(deleted).isFalse();
+    }
+}

Reply via email to