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

davsclaus pushed a commit to branch CAMEL-25764-dynamic-routing
in repository https://gitbox.apache.org/repos/asf/camel-jbang-examples.git

commit 89fc4183e766178ded6cb701ba34b6948a997a6d
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 18 15:01:52 2026 +0200

    CAMEL-25764: route/dynamic-routing - recipient list, failover load balancer 
and throttle
    
    A recipient list computes the endpoints from each order: the warehouse of
    its country and the billing of its status. Billing a paid order fails over
    from a primary invoice service that is down to a backup one, and the US
    warehouse is throttled to one order per second. Sixth in the route group,
    with a Citrus YAML test in the CI matrix.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .github/workflows/build.yml                        |   2 +
 README.md                                          |   1 +
 camel-jbang-example-catalog.json                   |  49 +++++++++
 route/README.md                                    |   1 +
 route/dynamic-routing/README.md                    |  82 +++++++++++++++
 route/dynamic-routing/application.properties       |  16 +++
 route/dynamic-routing/dynamic-routing.camel.yaml   | 111 +++++++++++++++++++++
 route/dynamic-routing/metadata.json                |  38 +++++++
 route/dynamic-routing/orders/order-1001.json       |   2 +
 route/dynamic-routing/orders/order-1002.json       |   2 +
 route/dynamic-routing/orders/order-1003.json       |   2 +
 .../test/dynamic-routing.citrus.it.yaml            |  35 +++++++
 12 files changed, 341 insertions(+)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 05cc0cd..a9fe25d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -74,6 +74,8 @@ jobs:
             path: route/aggregator
           - name: Filter and multicast
             path: route/filter-and-multicast
+          - name: Dynamic routing
+            path: route/dynamic-routing
           - name: Error handling
             path: fail-well/error-handling
           - name: Circuit breaker
diff --git a/README.md b/README.md
index 290139b..b4cb071 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,7 @@ The routing patterns: content-based router, splitter, 
aggregator, filter and mul
 | [Order lines](route/order-lines/) | Each order read from the orders 
directory is split into one message per line, the order id travels along in a 
header, and the log shows the order, one pick line per line, and the parent's 
confirmation that all lines went to picking. | nothing |
 | [Aggregator](route/aggregator/) | The warehouse reports each picked line on 
its own and the aggregator collects the lines of one order back into a 
shipment, correlated by the order id and complete when as many lines are in as 
the order had; each shipment is logged as JSON. | nothing |
 | [Filter and multicast](route/filter-and-multicast/) | Three orders are read 
from the orders directory; a filter lets only the paid ones through and a 
multicast sends each paid order to both the warehouse route and the invoicing 
route, which log their part; the pending order is logged as received and goes 
no further. | nothing |
+| [Dynamic routing](route/dynamic-routing/) | Each of the three orders goes to 
the warehouse of its country and to the billing of its status through a 
recipient list computed from the order; billing a paid order fails over from a 
primary invoice service that is down to a backup one, the pending order gets no 
invoice, and the US warehouse is throttled to one order per second. | nothing |
 
 ### [Fail well](fail-well/)
 
diff --git a/camel-jbang-example-catalog.json b/camel-jbang-example-catalog.json
index 0be9e36..30dcdd2 100644
--- a/camel-jbang-example-catalog.json
+++ b/camel-jbang-example-catalog.json
@@ -1047,6 +1047,55 @@
         ],
         "order": 1
     },
+    {
+        "name": "route/dynamic-routing",
+        "title": "Dynamic routing",
+        "description": "Each of the three orders goes to the warehouse of its 
country and to the billing of its status through a recipient list computed from 
the order; billing a paid order fails over from a primary invoice service that 
is down to a backup one, the pending order gets no invoice, and the US 
warehouse is throttled to one order per second.",
+        "level": "route",
+        "teaches": {
+            "components": [
+                "file",
+                "direct",
+                "log"
+            ],
+            "eips": [
+                "recipientList",
+                "loadBalance",
+                "failoverLoadBalancer",
+                "throttle",
+                "throwException",
+                "noErrorHandler",
+                "unmarshal"
+            ],
+            "languages": [
+                "simple",
+                "constant"
+            ],
+            "dataformats": [
+                "json"
+            ]
+        },
+        "tags": [
+            "shop",
+            "eip",
+            "recipient-list",
+            "failover",
+            "load-balancer",
+            "throttle"
+        ],
+        "bundled": true,
+        "requiresDocker": false,
+        "hasCitrusTests": true,
+        "files": [
+            "README.md",
+            "application.properties",
+            "dynamic-routing.camel.yaml",
+            "orders/order-1001.json",
+            "orders/order-1002.json",
+            "orders/order-1003.json"
+        ],
+        "order": 6
+    },
     {
         "name": "route/filter-and-multicast",
         "title": "Filter and multicast",
diff --git a/route/README.md b/route/README.md
index d881b2e..7ce8648 100644
--- a/route/README.md
+++ b/route/README.md
@@ -9,6 +9,7 @@ The routing patterns: content-based router, splitter, 
aggregator, filter and mul
 | [Order lines](order-lines/) | Each order read from the orders directory is 
split into one message per line, the order id travels along in a header, and 
the log shows the order, one pick line per line, and the parent's confirmation 
that all lines went to picking. | nothing |
 | [Aggregator](aggregator/) | The warehouse reports each picked line on its 
own and the aggregator collects the lines of one order back into a shipment, 
correlated by the order id and complete when as many lines are in as the order 
had; each shipment is logged as JSON. | nothing |
 | [Filter and multicast](filter-and-multicast/) | Three orders are read from 
the orders directory; a filter lets only the paid ones through and a multicast 
sends each paid order to both the warehouse route and the invoicing route, 
which log their part; the pending order is logged as received and goes no 
further. | nothing |
+| [Dynamic routing](dynamic-routing/) | Each of the three orders goes to the 
warehouse of its country and to the billing of its status through a recipient 
list computed from the order; billing a paid order fails over from a primary 
invoice service that is down to a backup one, the pending order gets no 
invoice, and the US warehouse is throttled to one order per second. | nothing |
 
 Start with [Content-based router](content-based-router/); the examples read 
best in the order above, each one building on what the one before it set up.
 
diff --git a/route/dynamic-routing/README.md b/route/dynamic-routing/README.md
new file mode 100644
index 0000000..16e3c56
--- /dev/null
+++ b/route/dynamic-routing/README.md
@@ -0,0 +1,82 @@
+# Dynamic routing
+
+Where an order goes is computed from the order itself: a recipient list sends 
it to the warehouse of its country
+and to the billing of its status. Billing fails over to a backup invoice 
service when the primary one is down, and
+the US warehouse takes at most one order per second.
+
+## What you will see
+
+```text
+INFO ... dynamic-routing.camel.yaml:16 : Order ORD-1001 for DK, paid
+INFO ... dynamic-routing.camel.yaml:30 : Warehouse DK picks ORD-1001
+INFO ... dynamic-routing.camel.yaml:110 : Invoice for ORD-1001 via the backup 
service
+...
+INFO ... dynamic-routing.camel.yaml:16 : Order ORD-1003 for US, pending
+INFO ... dynamic-routing.camel.yaml:56 : Warehouse US picks ORD-1003
+INFO ... dynamic-routing.camel.yaml:86 : No invoice for ORD-1003 until it is 
paid
+```
+
+## Install Camel CLI
+
+Install [JBang](https://www.jbang.dev/download/) and the Camel CLI as 
described in the
+[root README](../../README.md#install-the-camel-cli); `camel --version` 
confirms the install.
+
+## Run it
+
+```shell
+camel run *
+```
+
+Stop the example with `ctrl` + `c`.
+
+## How it works
+
+- `recipientList` computes the endpoints for each order with Simple:
+  `direct:warehouse-${body[country]},direct:billing-${body[status]}`. ORD-1001 
goes to `direct:warehouse-DK` and
+  `direct:billing-paid`, ORD-1003 to `direct:warehouse-US` and 
`direct:billing-pending`. Each endpoint is a route.
+- `billing-paid` sends the order through a `failoverLoadBalancer`: the primary 
invoice service first, and the backup
+  when the primary fails with an `IllegalStateException`. 
`inheritErrorHandler: false` makes it fail over at once,
+  without the route's error handler logging the failure first.
+- `invoice-primary` is down: it throws the exception, and `noErrorHandler` 
hands the failure straight back to the
+  load balancer.
+- `throttle` lets at most one order per second into the US warehouse 
(`timePeriodMillis: 1000`); an order over the
+  limit waits.
+
+## Build it step by step
+
+Ask your assistant, or type it yourself, one step at a time, and run after 
each:
+
+1. A file route on `orders` that unmarshals each order and logs its country 
and status.
+2. A recipient list to `direct:warehouse-<country>` and 
`direct:billing-<status>`, with a route for each.
+3. Bill a paid order through a failover load balancer: a primary invoice 
service that fails, and a backup that
+   takes over.
+4. Throttle the US warehouse to one order per second.
+
+## Try changing
+
+- Copy `order-1003.json` to `order-1004.json` and `order-1005.json` with new 
order ids: the US warehouse picks
+  them one second apart.
+- Remove the `throwException` from `invoice-primary`: every invoice goes 
through the primary service.
+- Add an order for a country without a warehouse route: the recipient list 
fails on the unknown endpoint, unless
+  you set `ignoreInvalidEndpoints: true`.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/) 
YAML DSL,
+`test/dynamic-routing.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/dynamic-routing.citrus.it.yaml
+```
+
+The test runs the routes and verifies the warehouses, the failover and the 
pending order.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git a/route/dynamic-routing/application.properties 
b/route/dynamic-routing/application.properties
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/route/dynamic-routing/application.properties
@@ -0,0 +1,16 @@
+# 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.
diff --git a/route/dynamic-routing/dynamic-routing.camel.yaml 
b/route/dynamic-routing/dynamic-routing.camel.yaml
new file mode 100644
index 0000000..e9f5f7e
--- /dev/null
+++ b/route/dynamic-routing/dynamic-routing.camel.yaml
@@ -0,0 +1,111 @@
+# Each order goes to the warehouse of its country and to the billing of its 
status: a recipient list computes both
+# endpoints from the order. Billing a paid order fails over to a backup 
invoice service when the primary one is down,
+# and the US warehouse takes at most one order per second.
+- route:
+    id: incoming-orders
+    from:
+      uri: file
+      parameters:
+        directoryName: orders
+        noop: true
+        sortBy: "file:name"
+      steps:
+        - unmarshal:
+            json:
+              library: Jackson
+        - log:
+            message: "Order ${body[orderId]} for ${body[country]}, 
${body[status]}"
+        - recipientList:
+            expression:
+              simple:
+                expression: 
"direct:warehouse-${body[country]},direct:billing-${body[status]}"
+
+- route:
+    id: warehouse-DK
+    from:
+      uri: direct
+      parameters:
+        name: warehouse-DK
+      steps:
+        - log:
+            message: "Warehouse DK picks ${body[orderId]}"
+
+- route:
+    id: warehouse-DE
+    from:
+      uri: direct
+      parameters:
+        name: warehouse-DE
+      steps:
+        - log:
+            message: "Warehouse DE picks ${body[orderId]}"
+
+# At most one order per second: an order over the limit waits
+- route:
+    id: warehouse-US
+    from:
+      uri: direct
+      parameters:
+        name: warehouse-US
+      steps:
+        - throttle:
+            expression:
+              constant:
+                expression: "1"
+            timePeriodMillis: 1000
+        - log:
+            message: "Warehouse US picks ${body[orderId]}"
+
+# The failover load balancer tries the primary invoice service first, and the 
backup when it fails
+- route:
+    id: billing-paid
+    from:
+      uri: direct
+      parameters:
+        name: billing-paid
+      steps:
+        - loadBalance:
+            failoverLoadBalancer:
+              # fail over at once, without the route's error handler logging 
the failure first
+              inheritErrorHandler: false
+              exception:
+                - java.lang.IllegalStateException
+            steps:
+              - to:
+                  uri: direct:invoice-primary
+              - to:
+                  uri: direct:invoice-backup
+
+- route:
+    id: billing-pending
+    from:
+      uri: direct
+      parameters:
+        name: billing-pending
+      steps:
+        - log:
+            message: "No invoice for ${body[orderId]} until it is paid"
+
+# The primary invoice service is down; noErrorHandler hands the failure 
straight back to the load balancer
+- route:
+    id: invoice-primary
+    errorHandler:
+      noErrorHandler: {}
+    from:
+      uri: direct
+      parameters:
+        name: invoice-primary
+      steps:
+        - throwException:
+            exceptionType: java.lang.IllegalStateException
+            message: "The invoice service is down"
+
+- route:
+    id: invoice-backup
+    from:
+      uri: direct
+      parameters:
+        name: invoice-backup
+      steps:
+        - log:
+            message: "Invoice for ${body[orderId]} via the backup service"
diff --git a/route/dynamic-routing/metadata.json 
b/route/dynamic-routing/metadata.json
new file mode 100644
index 0000000..ecb6801
--- /dev/null
+++ b/route/dynamic-routing/metadata.json
@@ -0,0 +1,38 @@
+{
+    "title": "Dynamic routing",
+    "description": "Each of the three orders goes to the warehouse of its 
country and to the billing of its status through a recipient list computed from 
the order; billing a paid order fails over from a primary invoice service that 
is down to a backup one, the pending order gets no invoice, and the US 
warehouse is throttled to one order per second.",
+    "level": "route",
+    "order": 6,
+    "teaches": {
+        "components": [
+            "file",
+            "direct",
+            "log"
+        ],
+        "eips": [
+            "recipientList",
+            "loadBalance",
+            "failoverLoadBalancer",
+            "throttle",
+            "throwException",
+            "noErrorHandler",
+            "unmarshal"
+        ],
+        "languages": [
+            "simple",
+            "constant"
+        ],
+        "dataformats": [
+            "json"
+        ]
+    },
+    "tags": [
+        "shop",
+        "eip",
+        "recipient-list",
+        "failover",
+        "load-balancer",
+        "throttle"
+    ],
+    "bundled": true
+}
diff --git a/route/dynamic-routing/orders/order-1001.json 
b/route/dynamic-routing/orders/order-1001.json
new file mode 100644
index 0000000..3eb5f03
--- /dev/null
+++ b/route/dynamic-routing/orders/order-1001.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status": "paid",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku": 
"CAMEL-MUG", "qty": 1, "price": 9.50}]}
diff --git a/route/dynamic-routing/orders/order-1002.json 
b/route/dynamic-routing/orders/order-1002.json
new file mode 100644
index 0000000..bb7aa0e
--- /dev/null
+++ b/route/dynamic-routing/orders/order-1002.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1002", "customer": "C-207", "country": "DE", "status": "paid",
+ "lines": [{"sku": "CAMEL-MUG", "qty": 3, "price": 9.50}]}
diff --git a/route/dynamic-routing/orders/order-1003.json 
b/route/dynamic-routing/orders/order-1003.json
new file mode 100644
index 0000000..9edab76
--- /dev/null
+++ b/route/dynamic-routing/orders/order-1003.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1003", "customer": "C-134", "country": "US", "status": 
"pending",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1, "price": 19.95}, {"sku": 
"CAMEL-CAP", "qty": 1, "price": 14.00}, {"sku": "CAMEL-MUG", "qty": 2, "price": 
9.50}]}
diff --git a/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml 
b/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
new file mode 100644
index 0000000..4794852
--- /dev/null
+++ b/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
@@ -0,0 +1,35 @@
+name: dynamic-routing-test
+description: The dynamic-routing example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "dynamic-routing"
+            file: "../dynamic-routing.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "Warehouse DK picks ORD-1001"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "Invoice for ORD-1001 via the backup service"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "Warehouse US picks ORD-1003"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "No invoice for ORD-1003 until it is paid"

Reply via email to