[
https://issues.apache.org/jira/browse/CAMEL-23239?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen updated CAMEL-23239:
--------------------------------
Fix Version/s: 4.20.0
(was: 4.19.0)
> 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
> Reporter: Guillaume Nodet
> Assignee: Guillaume Nodet
> Priority: Major
> Fix For: 4.20.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)