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

davsclaus pushed a commit to branch feature/CAMEL-23680-group-variable-scope
in repository https://gitbox.apache.org/repos/asf/camel.git

commit ac96324e6e987314e65ee0c37273b1582bc103f0
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jun 7 09:15:59 2026 +0200

    CAMEL-23680: Add group as scope to variables
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/spi/VariableRepositoryFactory.java       |   7 +
 .../engine/DefaultVariableRepositoryFactory.java   |  20 ++
 .../camel/processor/SetGroupVariableTest.java      | 126 ++++++++++++
 .../camel/support/GroupVariableRepositoryTest.java | 194 +++++++++++++++++++
 .../camel/main/MainSupportModelConfigurer.java     |   4 +
 .../camel/support/GroupVariableRepository.java     | 215 +++++++++++++++++++++
 docs/user-manual/modules/ROOT/pages/variables.adoc |  38 +++-
 7 files changed, 600 insertions(+), 4 deletions(-)

diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/VariableRepositoryFactory.java
 
b/core/camel-api/src/main/java/org/apache/camel/spi/VariableRepositoryFactory.java
index 749f56adcfd3..4a190295edc2 100644
--- 
a/core/camel-api/src/main/java/org/apache/camel/spi/VariableRepositoryFactory.java
+++ 
b/core/camel-api/src/main/java/org/apache/camel/spi/VariableRepositoryFactory.java
@@ -35,6 +35,13 @@ public interface VariableRepositoryFactory {
      */
     String ROUTE_VARIABLE_REPOSITORY_ID = "route-variable-repository";
 
+    /**
+     * Registry bean id for group {@link VariableRepository}.
+     *
+     * @since 4.21
+     */
+    String GROUP_VARIABLE_REPOSITORY_ID = "group-variable-repository";
+
     /**
      * Gets the {@link VariableRepository} for the given id
      *
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultVariableRepositoryFactory.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultVariableRepositoryFactory.java
index 8d834c145edb..d130c8c282b5 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultVariableRepositoryFactory.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultVariableRepositoryFactory.java
@@ -27,6 +27,7 @@ import org.apache.camel.spi.VariableRepository;
 import org.apache.camel.spi.VariableRepositoryFactory;
 import org.apache.camel.support.CamelContextHelper;
 import org.apache.camel.support.GlobalVariableRepository;
+import org.apache.camel.support.GroupVariableRepository;
 import org.apache.camel.support.LifecycleStrategySupport;
 import org.apache.camel.support.RouteVariableRepository;
 import org.apache.camel.support.service.ServiceSupport;
@@ -45,6 +46,7 @@ public class DefaultVariableRepositoryFactory extends 
ServiceSupport implements
     private final CamelContext camelContext;
     private VariableRepository global;
     private VariableRepository route;
+    private VariableRepository group;
     private FactoryFinder factoryFinder;
 
     public DefaultVariableRepositoryFactory(CamelContext camelContext) {
@@ -64,6 +66,9 @@ public class DefaultVariableRepositoryFactory extends 
ServiceSupport implements
         if (route != null && "route".equals(id)) {
             return route;
         }
+        if (group != null && "group".equals(id)) {
+            return group;
+        }
 
         VariableRepository repo = CamelContextHelper.lookup(camelContext, id, 
VariableRepository.class);
         if (repo == null) {
@@ -119,6 +124,18 @@ public class DefaultVariableRepositoryFactory extends 
ServiceSupport implements
             camelContext.getRegistry().bind(ROUTE_VARIABLE_REPOSITORY_ID, 
route);
         }
 
+        // let's see if there is a custom group repo
+        repo = getVariableRepository("group");
+        if (repo != null) {
+            if (!(repo instanceof GroupVariableRepository)) {
+                LOG.info("Using VariableRepository: {} as group repository", 
repo.getId());
+            }
+            group = repo;
+        } else {
+            group = new GroupVariableRepository();
+            camelContext.getRegistry().bind(GROUP_VARIABLE_REPOSITORY_ID, 
group);
+        }
+
         if (!camelContext.hasService(global)) {
             camelContext.addService(global);
         }
@@ -134,6 +151,9 @@ public class DefaultVariableRepositoryFactory extends 
ServiceSupport implements
                 }
             });
         }
+        if (!camelContext.hasService(group)) {
+            camelContext.addService(group);
+        }
     }
 
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/SetGroupVariableTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/SetGroupVariableTest.java
new file mode 100644
index 000000000000..fe7ce711b973
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/SetGroupVariableTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.processor;
+
+import java.util.List;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class SetGroupVariableTest extends ContextTestSupport {
+
+    private MockEndpoint end;
+
+    @Test
+    public void testSetGroupVariable() throws Exception {
+        assertNull(context.getVariable("group:teamA:foo"));
+        assertNull(context.getVariable("group:teamB:foo"));
+
+        end.expectedMessageCount(2);
+
+        template.sendBody("direct:teamA", "<blah/>");
+        template.sendBody("direct:teamB", "<blah/>");
+
+        assertMockEndpointsSatisfied();
+
+        // variables should be stored on exchange, not accessible from 
exchange directly
+        List<Exchange> exchanges = end.getExchanges();
+        Exchange exchange = exchanges.get(0);
+        assertNull(exchange.getVariable("foo"));
+
+        // should be stored as group variables
+        assertEquals("bar", context.getVariable("group:teamA:foo"));
+        assertEquals("baz", context.getVariable("group:teamB:foo"));
+    }
+
+    @Test
+    public void testGroupVariableIsolation() throws Exception {
+        end.expectedMessageCount(1);
+
+        template.sendBody("direct:teamA", "<blah/>");
+
+        assertMockEndpointsSatisfied();
+
+        // teamA has the variable, teamB does not
+        assertEquals("bar", context.getVariable("group:teamA:foo"));
+        assertNull(context.getVariable("group:teamB:foo"));
+    }
+
+    @Test
+    public void testGroupVariableSimpleLanguage() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceived("Value is bar");
+
+        template.sendBody("direct:simple", "<blah/>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    public void testCrossRouteGroupVariable() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:cross");
+        mock.expectedBodiesReceived("shared-value");
+
+        // route "setter" sets the group variable, route "reader" reads it
+        template.sendBody("direct:setter", "<blah/>");
+        template.sendBody("direct:reader", "<blah/>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Override
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setUp();
+        end = getMockEndpoint("mock:end");
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("direct:teamA").routeId("routeA")
+                        .setVariable("group:teamA:foo").constant("bar")
+                        .to("mock:end");
+
+                from("direct:teamB").routeId("routeB")
+                        .setVariable("group:teamB:foo").constant("baz")
+                        .to("mock:end");
+
+                from("direct:simple").routeId("routeSimple")
+                        .setVariable("group:teamA:foo").constant("bar")
+                        .transform().simple("Value is 
${variable.group:teamA:foo}")
+                        .to("mock:result");
+
+                from("direct:setter").routeId("routeSetter")
+                        
.setVariable("group:shared:myKey").constant("shared-value")
+                        .to("mock:end");
+
+                from("direct:reader").routeId("routeReader")
+                        .setBody().variable("group:shared:myKey")
+                        .to("mock:cross");
+            }
+        };
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/GroupVariableRepositoryTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/GroupVariableRepositoryTest.java
new file mode 100644
index 000000000000..435acaece322
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/GroupVariableRepositoryTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.support;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GroupVariableRepositoryTest {
+
+    private GroupVariableRepository repo;
+
+    @BeforeEach
+    public void setUp() {
+        repo = new GroupVariableRepository();
+    }
+
+    @Test
+    public void testGetId() {
+        assertEquals("group", repo.getId());
+    }
+
+    @Test
+    public void testSetAndGetVariable() {
+        repo.setVariable("teamA:foo", "bar");
+        assertEquals("bar", repo.getVariable("teamA:foo"));
+    }
+
+    @Test
+    public void testVariableIsolationBetweenGroups() {
+        repo.setVariable("teamA:key", "valueA");
+        repo.setVariable("teamB:key", "valueB");
+
+        assertEquals("valueA", repo.getVariable("teamA:key"));
+        assertEquals("valueB", repo.getVariable("teamB:key"));
+    }
+
+    @Test
+    public void testMultipleVariablesInSameGroup() {
+        repo.setVariable("teamA:foo", "1");
+        repo.setVariable("teamA:bar", "2");
+        repo.setVariable("teamA:baz", "3");
+
+        assertEquals("1", repo.getVariable("teamA:foo"));
+        assertEquals("2", repo.getVariable("teamA:bar"));
+        assertEquals("3", repo.getVariable("teamA:baz"));
+        assertEquals(3, repo.size());
+    }
+
+    @Test
+    public void testGetNonExistentVariable() {
+        assertNull(repo.getVariable("teamA:missing"));
+    }
+
+    @Test
+    public void testGetNonExistentGroup() {
+        assertNull(repo.getVariable("noSuchGroup:key"));
+    }
+
+    @Test
+    public void testRemoveVariable() {
+        repo.setVariable("teamA:foo", "bar");
+        Object removed = repo.removeVariable("teamA:foo");
+
+        assertEquals("bar", removed);
+        assertNull(repo.getVariable("teamA:foo"));
+    }
+
+    @Test
+    public void testRemoveWildcard() {
+        repo.setVariable("teamA:foo", "1");
+        repo.setVariable("teamA:bar", "2");
+        repo.setVariable("teamB:baz", "3");
+
+        repo.removeVariable("teamA:*");
+
+        assertNull(repo.getVariable("teamA:foo"));
+        assertNull(repo.getVariable("teamA:bar"));
+        assertEquals("3", repo.getVariable("teamB:baz"));
+        assertEquals(1, repo.size());
+    }
+
+    @Test
+    public void testSetNullRemoves() {
+        repo.setVariable("teamA:foo", "bar");
+        repo.setVariable("teamA:foo", null);
+
+        assertNull(repo.getVariable("teamA:foo"));
+    }
+
+    @Test
+    public void testGetGroupIds() {
+        repo.setVariable("teamA:foo", "1");
+        repo.setVariable("teamB:bar", "2");
+        repo.setVariable("teamC:baz", "3");
+
+        Set<String> ids = repo.getGroupIds();
+        assertEquals(Set.of("teamA", "teamB", "teamC"), ids);
+    }
+
+    @Test
+    public void testGetGroupIdsEmpty() {
+        assertTrue(repo.getGroupIds().isEmpty());
+    }
+
+    @Test
+    public void testGetGroupIdsAfterRemoveWildcard() {
+        repo.setVariable("teamA:foo", "1");
+        repo.setVariable("teamB:bar", "2");
+
+        repo.removeVariable("teamA:*");
+
+        assertEquals(Set.of("teamB"), repo.getGroupIds());
+    }
+
+    @Test
+    public void testNames() {
+        repo.setVariable("teamA:foo", "1");
+        repo.setVariable("teamB:bar", "2");
+
+        Set<String> names = repo.names().collect(Collectors.toSet());
+        assertEquals(Set.of("teamA:foo", "teamB:bar"), names);
+    }
+
+    @Test
+    public void testGetVariables() {
+        repo.setVariable("teamA:foo", "1");
+        repo.setVariable("teamB:bar", "2");
+
+        Map<String, Object> vars = repo.getVariables();
+        assertEquals(2, vars.size());
+        assertEquals("1", vars.get("teamA:foo"));
+        assertEquals("2", vars.get("teamB:bar"));
+    }
+
+    @Test
+    public void testHasVariables() {
+        assertFalse(repo.hasVariables());
+
+        repo.setVariable("teamA:foo", "bar");
+        assertTrue(repo.hasVariables());
+    }
+
+    @Test
+    public void testSize() {
+        assertEquals(0, repo.size());
+
+        repo.setVariable("teamA:foo", "1");
+        repo.setVariable("teamA:bar", "2");
+        repo.setVariable("teamB:baz", "3");
+        assertEquals(3, repo.size());
+    }
+
+    @Test
+    public void testClear() {
+        repo.setVariable("teamA:foo", "1");
+        repo.setVariable("teamB:bar", "2");
+
+        repo.clear();
+
+        assertFalse(repo.hasVariables());
+        assertEquals(0, repo.size());
+    }
+
+    @Test
+    public void testMissingColonThrows() {
+        assertThrows(IllegalArgumentException.class, () -> 
repo.getVariable("noColon"));
+        assertThrows(IllegalArgumentException.class, () -> 
repo.setVariable("noColon", "value"));
+        assertThrows(IllegalArgumentException.class, () -> 
repo.removeVariable("noColon"));
+    }
+}
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/MainSupportModelConfigurer.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/MainSupportModelConfigurer.java
index 117d75e87b6d..935f7347617a 100644
--- 
a/core/camel-main/src/main/java/org/apache/camel/main/MainSupportModelConfigurer.java
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/MainSupportModelConfigurer.java
@@ -101,6 +101,10 @@ public final class MainSupportModelConfigurer {
                 id = "route";
                 key = key.substring(6);
                 key = StringHelper.replaceFirst(key, ".", ":");
+            } else if (key.startsWith("group.")) {
+                id = "group";
+                key = key.substring(6);
+                key = StringHelper.replaceFirst(key, ".", ":");
             } else if (key.startsWith("global.")) {
                 id = "global";
                 key = key.substring(7);
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/GroupVariableRepository.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/GroupVariableRepository.java
new file mode 100644
index 000000000000..a9f9e33d94ee
--- /dev/null
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/GroupVariableRepository.java
@@ -0,0 +1,215 @@
+/*
+ * 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.support;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.StreamCache;
+import org.apache.camel.StreamCacheException;
+import org.apache.camel.spi.BrowsableVariableRepository;
+import org.apache.camel.spi.StreamCachingStrategy;
+import org.apache.camel.spi.VariableRepository;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.StringHelper;
+
+/**
+ * Group {@link VariableRepository} which stores variables in-memory per named 
group.
+ * <p>
+ * Variables are scoped by group name using the syntax {@code 
groupId:variableName}. This allows sharing variables
+ * across a subset of routes — wider than per-route but narrower than global 
scope.
+ *
+ * @since 4.21
+ */
+public final class GroupVariableRepository extends ServiceSupport implements 
BrowsableVariableRepository, CamelContextAware {
+
+    private final Map<String, Map<String, Object>> groups = new 
ConcurrentHashMap<>();
+    private CamelContext camelContext;
+    private StreamCachingStrategy strategy;
+
+    @Override
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        this.camelContext = camelContext;
+    }
+
+    @Override
+    public Object getVariable(String name) {
+        String id = StringHelper.before(name, ":");
+        String key = StringHelper.after(name, ":");
+        if (id == null || key == null) {
+            throw new IllegalArgumentException("Name must be groupId:name 
syntax");
+        }
+        Object answer = null;
+        Map<String, Object> variables = groups.get(id);
+        if (variables != null) {
+            answer = variables.get(key);
+        }
+        if (answer instanceof StreamCache sc) {
+            // reset so the cache is ready to be used as a variable
+            sc.reset();
+        }
+        return answer;
+    }
+
+    @Override
+    public void setVariable(String name, Object value) {
+        String id = StringHelper.before(name, ":");
+        String key = StringHelper.after(name, ":");
+        if (id == null || key == null) {
+            throw new IllegalArgumentException("Name must be groupId:name 
syntax");
+        }
+
+        if (value != null && strategy != null) {
+            StreamCache sc = convertToStreamCache(value);
+            if (sc != null) {
+                value = sc;
+            }
+        }
+        if (value != null) {
+            Map<String, Object> variables = groups.computeIfAbsent(id, s -> 
new ConcurrentHashMap<>(8));
+            // avoid the NullPointException
+            variables.put(key, value);
+        } else {
+            // if the value is null, we just remove the key from the map
+            Map<String, Object> variables = groups.get(id);
+            if (variables != null) {
+                variables.remove(key);
+            }
+        }
+    }
+
+    public boolean hasVariables() {
+        for (var vars : groups.values()) {
+            if (!vars.isEmpty()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public int size() {
+        int size = 0;
+        for (var vars : groups.values()) {
+            size += vars.size();
+        }
+        return size;
+    }
+
+    public Stream<String> names() {
+        List<String> answer = new ArrayList<>();
+        for (Map.Entry<String, Map<String, Object>> entry : groups.entrySet()) 
{
+            String id = entry.getKey();
+            Map<String, Object> values = entry.getValue();
+            for (var e : values.entrySet()) {
+                answer.add(id + ":" + e.getKey());
+            }
+        }
+        return answer.stream();
+    }
+
+    public Map<String, Object> getVariables() {
+        Map<String, Object> answer = new ConcurrentHashMap<>();
+        for (Map.Entry<String, Map<String, Object>> entry : groups.entrySet()) 
{
+            String id = entry.getKey();
+            Map<String, Object> values = entry.getValue();
+            for (var e : values.entrySet()) {
+                answer.put(id + ":" + e.getKey(), e.getValue());
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Gets the ids of all groups that currently have variables.
+     */
+    public Set<String> getGroupIds() {
+        return Collections.unmodifiableSet(groups.keySet());
+    }
+
+    public void clear() {
+        groups.clear();
+    }
+
+    @Override
+    public String getId() {
+        return "group";
+    }
+
+    @Override
+    public Object removeVariable(String name) {
+        String id = StringHelper.before(name, ":");
+        String key = StringHelper.after(name, ":");
+        if (id == null || key == null) {
+            throw new IllegalArgumentException("Name must be groupId:name 
syntax");
+        }
+
+        Map<String, Object> variables = groups.get(id);
+        if (variables != null) {
+            if ("*".equals(key)) {
+                variables.clear();
+                groups.remove(id);
+                return null;
+            } else {
+                return variables.remove(key);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void doInit() throws Exception {
+        super.doInit();
+
+        if (camelContext != null && camelContext.isStreamCaching()) {
+            strategy = camelContext.getStreamCachingStrategy();
+        }
+    }
+
+    private StreamCache convertToStreamCache(Object body) {
+        // check if body is already cached
+        if (body == null) {
+            return null;
+        } else if (body instanceof StreamCache sc) {
+            // reset so the cache is ready to be used before processing
+            sc.reset();
+            return sc;
+        }
+        return tryStreamCache(body);
+    }
+
+    private StreamCache tryStreamCache(Object body) {
+        try {
+            // cache the body and if we could do that replace it as the new 
body
+            return strategy.cache(body);
+        } catch (Exception e) {
+            throw new StreamCacheException(body, e);
+        }
+    }
+
+}
diff --git a/docs/user-manual/modules/ROOT/pages/variables.adoc 
b/docs/user-manual/modules/ROOT/pages/variables.adoc
index 239019c0e855..8130a485d5ff 100644
--- a/docs/user-manual/modules/ROOT/pages/variables.adoc
+++ b/docs/user-manual/modules/ROOT/pages/variables.adoc
@@ -5,7 +5,7 @@
 In Camel 4.4, we have introduced the concept of _variables_.
 
 A variable is a key/value that can hold a value that can either be private per 
`Exchange`,
-or shared per route, or per `CamelContext`.
+or shared per route, per named group, or per `CamelContext`.
 
 IMPORTANT: You can also use _exchange properties_ as variables, but the 
exchange properties are also used internally by Camel,
 and some EIPs and components. With the newly introduced _variables_ then these 
are exclusively for end users.
@@ -16,6 +16,7 @@ The variables are stored in one or more 
`org.apache.camel.spi.VariableRepository
 
 - `ExchangeVariableRepository` - A private repository per `Exchange` that 
holds variables that are private for the lifecycle of each `Exchange`.
 - `RouteVariableRepository` - Uses `route` as id. A single repository, that 
holds variables per `Route`.
+- `GroupVariableRepository` - Uses `group` as id. A single repository, that 
holds variables per named group. This allows sharing variables across a subset 
of routes — wider than per-route but narrower than global scope.
 - `GlobalVariableRepository` - Uses `global` as id. A single global repository 
for the entire `CamelContext`.
 
 The `ExchangeVariableRepository` is special as its private per exchange and is 
the default repository when used during routing.
@@ -30,9 +31,9 @@ from management tooling, CLI, and the developer console.
 You can implement custom `org.apache.camel.spi.VariableRepository` and plugin 
to be used out of the box with Apache Camel.
 For example, you can build a custom repository that stores the variables in a 
database, so they are persisted.
 
-Each repository must have its own unique id. However, it's also possible to 
replace the default `global`, or `route` repositories with another.
+Each repository must have its own unique id. However, it's also possible to 
replace the default `global`, `route`, or `group` repositories with another.
 
-IMPORTANT: The id `exchange` and `header` is reserved by Camel internally and 
should not be used as id for custom repositories.
+IMPORTANT: The id `exchange`, `header`, `global`, `route`, and `group` are 
reserved by Camel internally and should not be used as id for custom 
repositories.
 
 == Getting and setting variables from Java API
 
@@ -93,6 +94,26 @@ Object val = 
context.getVariable("route:myRouteId:myRouteKey");
 String str = context.getVariable("route:myRouteId:myRouteKey", String.class);
 ----
 
+You can assign a variable to a named group with `group:` as follows:
+
+[source,java]
+----
+exchange.setVariable("group:teamA:myKey", someObjectHere);
+----
+
+And you can get group variables as well:
+
+[source,java]
+----
+Object val = exchange.getVariable("group:teamA:myKey");
+
+// you can get the value as a specific type
+String str = exchange.getVariable("group:teamA:myKey", String.class);
+----
+
+Group variables are shared across all routes that use the same group name,
+which makes them useful for sharing state between a subset of related routes.
+
 == Setting and getting variables from DSL
 
 It is also possible to use variables in Camel xref:routes.adoc[routes] using 
the:
@@ -233,7 +254,7 @@ YAML::
 
 == Configuring initial variables on startup
 
-When Camel is starting then it's possible to configure initial variables for 
`global` and `route` repositories only.
+When Camel is starting then it's possible to configure initial variables for 
`global`, `route`, and `group` repositories only.
 
 This can be done in `application.properties` as shown below:
 
@@ -256,6 +277,15 @@ camel.variable.random = 999
 
 Here the gold variable is set on the `route` repository, and the other 
variables are set on the `global` repository.
 
+You can also set group scoped variables, using `group.` as prefix. The dot is 
used to separate
+the group id from the variable name, eg `teamA.threshold`.
+
+[source,properties]
+----
+camel.variable.group.teamA.threshold = 100
+camel.variable.group.teamB.region = EU
+----
+
 The value of a variable can also be loaded from the file system, such as a 
JSon file. To do this, you should
 prefix the value with `resource:file:` or `resource:classpath:` to load from 
the file system or classpath,
 as shown below:

Reply via email to