[ 
https://issues.apache.org/jira/browse/CAMEL-23239?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Guillaume Nodet updated CAMEL-23239:
------------------------------------
    Description: 
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.

  was:
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-message TTL 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
* *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

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}

h3. Auto-discovery — no backend reference needed
{code:yaml}
- route:
    from:
      uri: direct:store
    steps:
      - setHeader:
          name: CamelStateStoreKey
          constant: myKey
      - to:
          uri: state-store:myStore?operation=put
{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.


> 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.19.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)

Reply via email to