[
https://issues.apache.org/jira/browse/CAMEL-23239?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18107475#comment-18107475
]
Guillaume Nodet commented on CAMEL-23239:
-----------------------------------------
{noformat:title=AI-generated content}
_Claude Code on behalf of gnodet_
{noformat}
Hi Raymond,
Thanks for the detailed feedback and the thoughtful vision — it's great to see
real-world use cases driving this.
The initial implementation is up for review in [PR
#22158|https://github.com/apache/camel/pull/22158]. It covers the core of what
you described: a simple key-value API with a pluggable SPI
({{StateStoreBackend}}) and four backends out of the box:
* *In-Memory* (ConcurrentHashMap with lazy TTL) — zero dependencies, good for
dev/test
* *Caffeine* — high-performance local cache with proper eviction
* *Redis* (via Redisson) — distributed, native per-entry TTL
* *Infinispan* (via Hot Rod) — distributed cache with lifespan-based TTL
All backends are auto-discovered from the Camel registry and can be configured
entirely via {{application.properties}} (no Java code needed), which should
work well for YAML DSL and JBang users.
Regarding your broader suggestions:
* *More backends* (MapDB, RocksDB, Hazelcast, etcd, etc.) — the SPI is designed
to make adding new backends straightforward. These would be great follow-up
contributions.
* *Variables integration* — interesting idea, worth exploring once the core
component is stable.
* *Unified SPI for internal Camel state* (AggregateRepository,
IdempotentRepository, etc.) — I've created
[CAMEL-24463|https://issues.apache.org/jira/browse/CAMEL-24463] to introduce a
{{KeyValueRepository}} SPI in {{camel-api}} that would serve as the common
foundation. The idea is that generic adapters
({{KeyValueIdempotentRepository}}, {{KeyValueAggregationRepository}}) would
delegate to any {{KeyValueRepository}} implementation, so adding a single new
backend gives you state store + idempotent consumer + aggregate EIP for free —
no more separate implementations per backend.
* *Encrypted secrets* — out of scope for the initial version, but could be
layered on top (e.g., a decorating backend that encrypts/decrypts values).
Once the PR is merged, I'd love to get your feedback on it — especially how it
compares to your current MongoDB/MapDB setup.
> Add camel-state-store component with pluggable key-value store
> --------------------------------------------------------------
>
> Key: CAMEL-23239
> URL: https://issues.apache.org/jira/browse/CAMEL-23239
> Project: Camel
> Issue Type: New Feature
> Components: camel-core
> Reporter: Guillaume Nodet
> Assignee: Guillaume Nodet
> Priority: Major
> Fix For: 4.23.0
>
>
> h2. Motivation
> Camel provides dedicated components for specific caching/store technologies
> (Caffeine, Redis, Infinispan, etc.), each with its own API surface. This
> works well when users need the full feature set of a specific technology, but
> it creates friction when the actual need is simple: store and retrieve
> key-value pairs.
> The {{camel-state-store}} component follows the same "choose the problem, not
> the technology" pattern that Camel already uses successfully in other areas:
> * {{camel-sql}} / {{camel-jdbc}} — generic SQL over any JDBC database,
> without locking into a vendor
> * {{camel-jms}} — generic messaging over any JMS provider (with
> {{camel-activemq}}, {{camel-amqp}} as pre-configured variants)
> * {{camel-jcache}} — generic caching via JSR-107 over any compliant
> implementation
> Similarly, {{camel-state-store}} provides a unified key-value API where:
> * Users choose the capability first ("I need a key-value store") rather than
> a specific technology
> * The backend is swappable without changing route logic — develop with
> in-memory, deploy with Redis or Infinispan
> * The API surface is intentionally minimal (put, get, delete, contains, keys,
> clear) — unlike the full-featured technology-specific components
> h2. Difference from existing cache components
> || || camel-state-store || camel-caffeine-cache / camel-infinispan / etc. ||
> | *Focus* | Simple key-value store abstraction | Full feature set of a
> specific technology |
> | *Backend* | Pluggable via {{StateStoreBackend}} interface | Fixed to one
> technology |
> | *API* | Minimal: put, get, delete, contains, keys, clear | Rich: queries,
> events, statistics, pub/sub, etc. |
> | *Use case* | Portability, simplicity, migration from MuleSoft Object Store
> | Deep integration with a specific product |
> h2. Features
> * New {{camel-state-store}} component providing a simple, unified key-value
> store API with pluggable backends
> * Supports operations: {{put}}, {{putIfAbsent}}, {{get}}, {{delete}},
> {{contains}}, {{keys}}, {{size}}, {{clear}}
> * Per-entry TTL with endpoint-level default and per-message override via
> {{CamelStateStoreTtl}} header
> * *Auto-discovery*: if a single {{StateStoreBackend}} bean is in the
> registry, it is used automatically — no {{backend=#beanName}} needed on
> endpoints. Logs a WARN when multiple backends are found.
> * *Property-based configuration*: backends can be fully configured via
> {{application.properties}} using {{camel.beans.*}} syntax — no Java code
> required
> * Multi-module structure with pluggable backends:
> ** {{camel-state-store}}: core + in-memory backend ({{ConcurrentHashMap}},
> lazy TTL)
> ** {{camel-state-store-caffeine}}: Caffeine cache with per-entry variable
> expiry
> ** {{camel-state-store-redis}}: Redisson {{RMapCache}} with native TTL
> ** {{camel-state-store-infinispan}}: Hot Rod client with lifespan TTL
> * Custom backends via {{StateStoreBackend}} interface and bean references
> * Thread-safe backend lifecycle: {{start()}} called once per backend,
> idempotent guards in all backends
> h2. Route examples
> h3. Simple put/get with in-memory backend
> {code:java}
> from("direct:store")
> .setHeader(StateStoreConstants.KEY, constant("user-123"))
> .to("state-store:sessions?operation=put");
> from("direct:lookup")
> .setHeader(StateStoreConstants.KEY, constant("user-123"))
> .to("state-store:sessions?operation=get")
> .log("Found: ${body}");
> {code}
> h3. Caching HTTP responses with TTL
> {code:java}
> from("timer:poll?period=60000")
> .setHeader(StateStoreConstants.KEY, constant("weather"))
> .to("state-store:cache?operation=get")
> .choice()
> .when(body().isNull())
> .to("https://api.weather.com/current")
> .setHeader(StateStoreConstants.KEY, constant("weather"))
> .setHeader(StateStoreConstants.TTL, constant(300000L))
> .to("state-store:cache?operation=put")
> .end()
> .to("direct:process-weather");
> {code}
> h3. Idempotent deduplication pattern
> {code:java}
> from("kafka:orders")
> .setHeader(StateStoreConstants.KEY, simple("${header.orderId}"))
> .to("state-store:processed?operation=putIfAbsent")
> .choice()
> .when(body().isNotNull())
> .log("Duplicate order ${header.orderId}, skipping")
> .stop()
> .end()
> .to("direct:process-order");
> {code}
> h3. YAML DSL with property-configured Redis backend
> {code:title=application.properties}
> camel.beans.redisBackend =
> #class:org.apache.camel.component.statestore.redis.RedisStateStoreBackend
> camel.beans.redisBackend.redisUrl = redis://redis:6379
> camel.beans.redisBackend.mapName = app-state
> {code}
> {code:yaml}
> - route:
> from:
> uri: direct:save
> steps:
> - setHeader:
> name: CamelStateStoreKey
> simple: "${header.userId}"
> - to:
> uri: state-store:preferences?operation=put
> - route:
> from:
> uri: direct:load
> steps:
> - setHeader:
> name: CamelStateStoreKey
> simple: "${header.userId}"
> - to:
> uri: state-store:preferences?operation=get
> {code}
> h2. Configuration examples
> h3. Java bean registration
> {code:java}
> @BindToRegistry("caffeineBackend")
> public CaffeineStateStoreBackend caffeine() {
> CaffeineStateStoreBackend backend = new CaffeineStateStoreBackend();
> backend.setMaximumSize(50_000);
> return backend;
> }
> {code}
> h3. Property-based configuration (no Java required)
> {code:title=Caffeine}
> camel.beans.caffeineBackend =
> #class:org.apache.camel.component.statestore.caffeine.CaffeineStateStoreBackend
> camel.beans.caffeineBackend.maximumSize = 50000
> {code}
> {code:title=Redis}
> camel.beans.redisBackend =
> #class:org.apache.camel.component.statestore.redis.RedisStateStoreBackend
> camel.beans.redisBackend.redisUrl = redis://myhost:6379
> camel.beans.redisBackend.mapName = my-app-state
> {code}
> {code:title=Infinispan}
> camel.beans.infinispanBackend =
> #class:org.apache.camel.component.statestore.infinispan.InfinispanStateStoreBackend
> camel.beans.infinispanBackend.hosts = myhost:11222
> camel.beans.infinispanBackend.cacheName = my-cache
> {code}
> h2. Design decision: StateStoreBackend interface location
> We considered moving the {{StateStoreBackend}} interface to {{camel-api}}
> alongside existing SPIs ({{IdempotentRepository}}, {{AggregationRepository}},
> {{StateRepository}}). We decided against it because:
> * The existing {{camel-api}} SPIs are there because core EIPs consume them
> (idempotent consumer, aggregator). {{StateStoreBackend}} is only consumed by
> the component itself.
> * The existing SPIs have different semantics (two-phase confirm/rollback,
> Exchange serialization) that don't map to a simple key-value store.
> * Moving it would add API surface with stricter stability guarantees for no
> practical benefit today.
> If a future core EIP needs a generic key-value store, the interface can be
> promoted then.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)