9.2 Scatter-Gather Router: Parallel Execution, Aggregation & Error Traps
Key Takeaways
- The Scatter-Gather router (<scatter-gather>) broadcasts an identical clone of the incoming Mule event to two or more parallel processing routes simultaneously on separate threads.
- Each parallel route executes in complete isolation; modifications made to the payload or variables in one route are invisible to other routes during execution.
- Upon successful completion of all routes, Scatter-Gather aggregates the results into a composite Mule event where payload is a Map keyed by route index string ("0", "1", "2", ...), with each entry containing the respective route's MuleMessage (payload and attributes).
- Variables created or modified in individual routes are merged back into the main flow context upon completion; if multiple routes modify the same variable name, the resulting value is non-deterministic.
- If an unhandled error occurs in ANY route, Scatter-Gather immediately halts normal aggregation and raises a MULE:COMPOSITE_ROUTING error containing an error.childErrors collection of all failures and partial successes.
Scatter-Gather Router: Parallel Execution, Aggregation & Error Traps
Aggregating information from multiple disparate backends—such as querying airline, hotel, and car rental services simultaneously—is a foundational integration requirement. In Mule 4, the Scatter-Gather Router (<scatter-gather>) enables concurrent, parallel processing across multiple routes, aggregating individual responses into a single composite Mule message. Understanding thread dispatching, event cloning, result aggregation, variable merging, and composite error handling is essential for mastering flow control in Mule applications.
1. Scatter-Gather Execution Architecture & Event Cloning
When a Mule event enters a <scatter-gather> router, the runtime performs the following sequence:
- Event Cloning: A separate, deep copy of the incoming Mule event (including
payload,attributes, andvars) is created for every declared<route>. - Parallel Dispatch: Each route is assigned to an independent thread from the Mule runtime reactive thread pool and executed concurrently.
- Thread Synchronization: The router waits for all parallel routes to complete (or for a timeout or unhandled exception to occur) before proceeding.
+-------------------------------------------------------------------------------------------------+
| SCATTER-GATHER PARALLEL ARCHITECTURE |
| |
| [ Incoming Mule Event ] |
| payload: { "tripId": "T-101" } |
| vars: { "userId": "U-88" } |
| | |
| v |
| +-----------------------------------+ |
| | <scatter-gather> Router | |
| +-----------------------------------+ |
| / | \ |
| [Clone Event Copy 0] [Clone Event Copy 1] [Clone Event Copy 2] |
| | | | |
| v v v |
| +---------------------+ +---------------------+ +---------------------+ |
| | Route 0 (Thread A) | | Route 1 (Thread B) | | Route 2 (Thread C) | |
| | <http:request> | | <http:request> | | <http:request> | |
| | Flight Partner API | | Hotel Partner API | | Car Rental API | |
| +---------------------+ +---------------------+ +---------------------+ |
| | | | |
| v v v |
| [Flight Message] [Hotel Message] [Car Rental Msg] |
| \ | / |
| \ | / |
| v v v |
| +-----------------------------------+ |
| | AGGREGATION ENGINE | |
| +-----------------------------------+ |
| | |
| v |
| [ Composite Mule Event ] |
| payload: { |
| "0": { attributes: { statusCode: 200 }, payload: { flights: [...] } }, |
| "1": { attributes: { statusCode: 200 }, payload: { hotels: [...] } }, |
| "2": { attributes: { statusCode: 200 }, payload: { cars: [...] } } |
| } |
| vars: { userId: "U-88", (merged variables from all routes) } |
+-------------------------------------------------------------------------------------------------+
XML Configuration Example:
<flow name="aggregate-travel-itinerary-flow" doc:name="Aggregate Travel Itinerary Flow">
<http:listener config-ref="HTTP_Listener_config" path="/itinerary" doc:name="Listener"/>
<set-variable variableName="bookingId" value="#[payload.bookingId]" doc:name="Set Booking ID"/>
<scatter-gather doc:name="Scatter-Gather: Aggregate Services" timeout="10000">
<!-- Route 0: Flights API -->
<route>
<http:request config-ref="Flights_HTTP_Config" path="/flights" method="GET" doc:name="Get Flights">
<http:query-params><![CDATA[#[{'bookingId': vars.bookingId}]]]></http:query-params>
</http:request>
<set-variable variableName="flightsStatus" value="SUCCESS" doc:name="Set Flights Status"/>
</route>
<!-- Route 1: Hotels API -->
<route>
<http:request config-ref="Hotels_HTTP_Config" path="/hotels" method="GET" doc:name="Get Hotels">
<http:query-params><![CDATA[#[{'bookingId': vars.bookingId}]]]></http:query-params>
</http:request>
<set-variable variableName="hotelsStatus" value="SUCCESS" doc:name="Set Hotels Status"/>
</route>
<!-- Route 2: Rental Cars API -->
<route>
<http:request config-ref="Cars_HTTP_Config" path="/cars" method="GET" doc:name="Get Cars">
<http:query-params><![CDATA[#[{'bookingId': vars.bookingId}]]]></http:query-params>
</http:request>
<set-variable variableName="carsStatus" value="SUCCESS" doc:name="Set Cars Status"/>
</route>
</scatter-gather>
<!-- Transform the aggregated composite payload into a unified response -->
<ee:transform doc:name="Aggregate Response">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
bookingId: vars.bookingId,
flights: payload["0"].payload,
hotels: payload["1"].payload,
cars: payload["2"].payload,
flightResponseCode: payload["0"].attributes.statusCode
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</flow>
2. Structure of the Aggregated Composite Message
When all routes finish successfully, the resulting payload is an Object / Map containing zero-based string indices ("0", "1", "2", ...) corresponding to the declaration order of the <route> elements.
Detailed Structure of payload After Scatter-Gather:
{
"0": {
"attributes": {
"statusCode": 200,
"headers": { "content-type": "application/json" }
},
"payload": {
"airline": "United",
"flightNumber": "UA-442",
"price": 350.00
}
},
"1": {
"attributes": {
"statusCode": 200,
"headers": { "content-type": "application/json" }
},
"payload": {
"hotelName": "Grand Hyatt",
"nights": 3,
"totalRate": 620.00
}
}
}
DataWeave Access Patterns:
- Direct Indexed Navigation:
payload["0"].payloadaccesses Route 0's payload body;payload["0"].attributes.statusCodeaccesses Route 0's attributes. - Dynamic Pluck Extraction:
payload pluck ((routeMessage) -> routeMessage.payload)converts all route payloads into a flat JSON array[ { flight... }, { hotel... } ]. - Descendant Extraction (
..):payload..priceextracts all fields namedpriceacross all child route payloads.
[!IMPORTANT]
payload["0"].payloadvspayload[0]In Mule 4, the output of a successful Scatter-Gather is a Map ofMuleMessageobjects, NOT a simple JSON Array. Accessingpayload[0]treats the payload as an indexed array and will fail or return null in DataWeave. You must access the route key as a map index:payload["0"].payloadorpayload."0".payload.
3. Variable Merging Rules & Collisions
How variables behave across parallel Scatter-Gather routes is a major certification focus:
+-----------------------------------------------------------------------------------------+
| VARIABLE MERGING IN SCATTER-GATHER |
| |
| BEFORE SCATTER-GATHER: |
| vars.sharedVar = "ORIGINAL" |
| |
| ROUTE 0 MODIFICATIONS: ROUTE 1 MODIFICATIONS: |
| vars.sharedVar = "FROM_ROUTE_0" vars.sharedVar = "FROM_ROUTE_1" |
| vars.varRoute0 = "DATA_0" vars.varRoute1 = "DATA_1" |
| |
| AFTER SCATTER-GATHER (MERGED): |
| vars.varRoute0 = "DATA_0" (Cleanly merged) |
| vars.varRoute1 = "DATA_1" (Cleanly merged) |
| vars.sharedVar = NON-DETERMINISTIC! (Either "FROM_ROUTE_0" or "FROM_ROUTE_1") |
+-----------------------------------------------------------------------------------------+
Variable Rules Summary:
- New Unique Variables: Any variable created in an individual route (e.g.,
vars.flightIdin Route 0,vars.hotelIdin Route 1) is cleanly merged into the parent flow's variable context and is available downstream. - Variable Collisions: If multiple parallel routes modify the same variable name (or modify an existing variable initialized before the router), the final value is non-deterministic (last thread to complete overwrites the value).
- Best Practice: Always use route-prefixed or uniquely named variables (e.g.,
vars.route0_result,vars.route1_result) inside parallel routes to eliminate collision risks.
4. Error Handling: MULE:COMPOSITE_ROUTING & Route Isolation
If ANY single route throws an unhandled error during execution:
- Scatter-Gather immediately cancels normal aggregation.
- Scatter-Gather raises a
MULE:COMPOSITE_ROUTINGerror. - Execution jumps immediately to the enclosing flow's error handler.
+-----------------------------------------------------------------------------------------+
| SCATTER-GATHER COMPOSITE ERROR SCENARIO |
| |
| Route 0: <http:request> ---> SUCCESS (HTTP 200 Flight Data) |
| Route 1: <http:request> ---> ERROR: HTTP:CONNECTIVITY (Hotel Service Unreachable!) |
| Route 2: <http:request> ---> SUCCESS (HTTP 200 Car Data) |
| |
| RESULT: Entire Scatter-Gather throws MULE:COMPOSITE_ROUTING |
| |
| IN ERROR HANDLER: |
| - error.description: Summary of composite failure |
| - error.childErrors: Map containing error objects from failed routes |
| error.childErrors["1"].errorType ===> HTTP:CONNECTIVITY |
| error.childErrors["1"].description ===> "HTTP GET failed: Connection refused" |
| - error.errorMessage.payload: Map containing both successes and errors |
+-----------------------------------------------------------------------------------------+
Route-Level Error Isolation Pattern (Using <try>)
In many business scenarios, a failure in one non-critical route (such as rental cars) should not abort the entire aggregation. To achieve fault tolerance, wrap the route's processors inside a <try> scope with an <on-error-continue> error handler:
<scatter-gather doc:name="Fault-Tolerant Scatter-Gather">
<!-- Route 0: Flight Service -->
<route>
<http:request config-ref="Flights_HTTP_Config" path="/flights" method="GET" doc:name="Get Flights"/>
</route>
<!-- Route 1: Hotel Service with Try-Catch Isolation -->
<route>
<try doc:name="Try Hotel Lookup">
<http:request config-ref="Hotels_HTTP_Config" path="/hotels" method="GET" doc:name="Get Hotels"/>
<error-handler>
<on-error-continue type="ANY" doc:name="Catch Hotel Failure">
<logger level="WARN" message="#['Hotel lookup failed: ' ++ error.description]" doc:name="Log Warn"/>
<!-- Set a fallback empty payload so Route 1 succeeds -->
<set-payload value="#[{ 'hotels': [], 'status': 'UNAVAILABLE' }]" doc:name="Fallback Payload"/>
</on-error-continue>
</error-handler>
</try>
</route>
</scatter-gather>
[!TIP] How
<on-error-continue>Protects Scatter-Gather Because<on-error-continue>catches the error inside Route 1 and returns a fallback payload, Route 1 completes successfully from the perspective of the Scatter-Gather router. Scatter-Gather aggregates the fallback payload intopayload["1"].payloadwithout throwing aMULE:COMPOSITE_ROUTINGexception.
5. Exam Watch: Core Scatter-Gather Scenarios
[!IMPORTANT] Composite Error Structure When a route fails, the overall error type is always
MULE:COMPOSITE_ROUTING. Individual route failures are inspected usingerror.childErrors["0"],error.childErrors["1"], etc.
[!WARNING] Concurrency & Thread Safety Parallel routes execute concurrently on separate threads. Never perform non-thread-safe modifications to external shared resources (like writing to the same local file or updating a shared static cache without locks) from inside parallel routes.
[!TIP] Timeout Attribute The
timeoutattribute on<scatter-gather timeout="5000">is defined in milliseconds. If any route takes longer than the timeout to complete, Scatter-Gather aborts and raises aMULE:COMPOSITE_ROUTINGcontaining aMULE:TIMEOUTchild error.
A Mule flow contains a Scatter-Gather router with two parallel routes. Route 0 invokes an Accounts REST API returning { "accountId": 101, "name": "Acme Corp" }. Route 1 invokes a Billing REST API returning { "balance": 450.00, "status": "CURRENT" }. Both routes complete successfully with HTTP 200 status codes. How should a developer configure a Transform Message component immediately following the Scatter-Gather to extract the account name and balance into a unified JSON object?
Before entering a Scatter-Gather router, a flow variable vars.orderStatus is initialized to "PENDING". Route 0 executes and sets vars.orderStatus = "APPROVED" and vars.route0Result = "FLIGHT_OK". Route 1 executes concurrently and sets vars.orderStatus = "REJECTED" and vars.route1Result = "HOTEL_OK". Both routes complete without error. What is the state of the flow variables immediately after exiting the Scatter-Gather router?
A Scatter-Gather router contains three parallel routes. Route 0 and Route 2 complete successfully with HTTP 200 responses. Route 1 throws an unhandled HTTP:CONNECTIVITY error because the backend service is offline. What error is raised by the Scatter-Gather router, and how can the developer inspect the specific failure details of Route 1 in the flow's error handler?
A developer is designing a flight booking application that calls three independent airline supplier APIs in parallel using a Scatter-Gather router. The business requirement states that if any single airline API is unavailable or returns an error, the flow must NOT fail; instead, it should aggregate the results from the remaining available airlines and return an empty list for the failing supplier. What configuration achieves this requirement?