Skip to content
Art2link ESB v2.02 LTS HomeDocumentationBlogContact
Patterns/Orchestration-replacement/Countdown loop

Countdown loop

One message carries an array; you want one outbound call per element but you do not want to debatch it into N independent messages. A countdown loop keeps the batch whole and iterates it on the bus: a counter variable gates a send port that processes one element, decrements, and re-publishes itself until the counter reaches zero.

Most orchestration-replacement patterns wait on correlation, which is still on the roadmap. The countdown loop is the one you can build today — precisely because it never needs to hold or merge state across instances. It is a strictly linear construct: within a single loop one pass is in flight at a time, and it self-correlates through a counter that only ever counts down.

iOrders:  3 → 2 → 1 → 0 InboundOrders 3 records BUS SQL send port subscribe: iOrders > 0 on exit: iOrders −= 1 SQL one record per pass index = iOrders − 1 re-publish as InboundOrders Null port subscribe: iOrders == 0 · flush

The receive port counts the repeating node into a variable on the way in. A send port subscribes to the message type and the counter being above zero; on each pass it reads the element at the current index, makes its call, decrements the counter, and emits a response set to the same message type — so the message re-enters the bus and the same port matches again. Each pass the counter shrinks by one. When it reaches zero the send port’s predicate is false, the loop stops, and a null port subscribing on iOrders == 0 absorbs the final pass so nothing dead-letters for want of a subscriber.

EXPRESSIONreceive port On Entry assignment
Variable.iOrders  ←  {{Message.Body}}.JPath("$.orders.length")
EXPRESSIONSQL send port subscription
{{Message.MessageType}} == "InboundOrders" && {{Variable.iOrders}} > 0
EXPRESSIONSQL send port On Exit assignment + adapter parameter
Variable.iOrders  ←  {{Variable.iOrders}} - 1
parameter         ←  {{Message.Body}}.JPath("$.orders[{{Variable.iOrders}} - 1]")

The index is zero-based, so the current element is iOrders - 1: the pass with the counter at 3 reads orders[2], the pass at 1 reads orders[0], and every element is covered exactly once. Because the binding token resolves before the call on each pass, the adapter sees a concrete index every time. The counter walks the array last-to-first; if processing order matters, capture the original count in a second variable at the receive port and index forward as total - iOrders instead.

Keep the body light by not carrying the array in it. Capture the whole payload once into a held variable on the way in — the same technique the claim check uses — so the message tossed around the bus only carries the counter and the message type. The send port reads each element out of the stored original. For a large batch, persist the payload once and carry only a token, so the body does not ride the bus on every pass.

One loop of a given type at a time. The counter is instantiated per flow instance, so stored values never overwrite each other — but every re-circulating pass shares the same message type and subscription on a shared bus, and without a correlation key nothing binds a re-published message back to its instance’s counter. Run a single loop of a given message type at a time: two batches of the same type looping at once can cross subscriptions. True concurrency — and any fan-in or merge across instances — needs correlation and timers, not yet available. On failure, route the failed pass through an exception handler: earlier passes are already committed, so this is at-least-up-to-failure, not all-or-nothing. For true rollback, reach for saga / compensation.