Run Independent Work in Parallel

Read Scatter-Gather and Parallel For Each results, handle composite failure and account for dependency capacity.

The order service can look up a customer and a catalogue independently. Neither answer is needed to start the other request. Waiting for them sequentially spends time without adding a dependency the business requires.

Parallel work can reduce that waiting, but it changes two things that matter more than the saving: the shape of the result and the boundary of a failure. The controlled services and the tiny inputs stay, so both changes remain visible.

Shared input: Customer lookup: Catalogue lookup. Scatter-Gather: Routes overlap: Wait for results. Joined payload: Route-indexed Messages: Extract each payload. A composite failure does not undo a successful remote side effect.

Open this checkpoint in ACB

Stop the previous local application, then use File → Open Folder to open book/checkpoints/17-parallel-work. Open src/main/mule/app.xml and use Flow List to select the named flow for each example. Start the controlled dependency in a separate terminal from the companion root with python3 book/stubs/server.py. Choose Run and Debug → Run Mule Application and wait for deployment. Save canvas edits and use Save and Hot-deploy to Local Runtime before repeating a request.

Run python3 book/run.py verify 17 from the companion root to exercise this running checkpoint with its synthetic fixtures. The verifier supplies requests and checks results; it does not start the ACB application. Keep the editor on this checkpoint while reading a failure so an old deployment cannot supply a misleading answer.

Different routes, one input

The customer lookup and the catalogue lookup are different work over the same input, and neither needs the other’s answer. That independence is what makes them candidates for one fan-out — if the catalogue lookup needed an identifier the customer lookup produced, the second call could not simply begin alongside the first, however the diagram is drawn.

Example 043 — Join customer and catalogue lookups

The complete source is in checkpoints/17-parallel-work/src/main/mule/app.xml.

Choose Flow List → parallel-lookups and expand Scatter-Gather. It contains two routes. Select the Request in each route; both use Dependency_HTTP and Method GET:

RouteRequest path
First, index 0/customers/C-42
Second, index 1/catalogue

Open the Transform Message after Scatter-Gather. Its payload script reads the joined messages:

%dw 2.0
output application/json
---
{customer: payload[0].payload.name, prices: payload[1].payload}

Scatter-Gather sends the incoming event down each route. It waits for the routes and returns route results indexed by route number. Each result contains a Mule Message, so payload[0].payload.name selects the first result’s message payload and then its customer name. payload[1].payload selects the catalogue object. The outer payload is not simply the customer’s response body.

GET /lab/parallel-lookups produces Dana and the PEN-01/PAD-22/CLP-08 prices from the controlled services. Its output shape is explicit because downstream code should not have to know which route number happened to call which system.

Variables written inside routes are route work, not a shared accumulator. Each route begins from the incoming variable context, and a change one route makes is not visible to another while they run — so an increment per route is not a safe count, because parallel paths do not execute a dependable sequence of updates to one mutable counter. Give each route its own result and combine those results after the join.

Scatter-Gather containing two separate Request routes

Scatter-Gather containing two separate Request routes.

One operation over several elements

The other fan-out shape spreads one processor sequence over a collection rather than over named routes. Its result is the part that catches people out: a collection of messages in input order, not a collection of the business values those messages carry.

Example 044 — Double independent items in parallel

The complete source is in checkpoints/17-parallel-work/src/main/mule/app.xml.

Choose Flow List → parallel-items. The initial Set Payload uses expression [1,2,3]. Select Parallel For Each and set Max Concurrency to 2. Inside that scope, Set Payload uses payload * 2 in expression mode.

Select the Transform Message after the scope. Its script extracts the payload from each result message:

%dw 2.0
output application/json
---
payload map (result) -> result.payload

Parallel For Each distributes elements through the same processors. Here the input is [1,2,3], each route multiplies its element by two, and the result collection contains messages whose payloads are 2, 4 and 6. The transform extracts those payloads into [2,4,6].

Max Concurrency 2 bounds simultaneous work within this scope — it does not impose a two-request limit across every HTTP request or application replica. If ten incoming requests each start their own scope, total dependency concurrency can be much larger.

The callback does no external work, so it is easy to predict. For an actual update, first ask whether element B depends on A’s committed result. If so, the operation is not independent merely because the source supplied an array.

A failed route still has an owner

One route succeeding does not make the request successful, and one route failing does not undo what another route has already done. Scatter-Gather makes an aggregate success-or-error decision — not an atomic transaction across downstream systems.

Example 045 — Handle a failed parallel lookup

The complete source is in checkpoints/17-parallel-work/src/main/mule/app.xml.

Choose Flow List → failed-parallel-route. Expand Scatter-Gather: its first Request calls /customers/C-42; its second calls /shipping/unavailable. Both use GET through Dependency_HTTP.

Expand the flow-level Error handler. On Error Continue matches MULE:COMPOSITE_ROUTING; it sets variable httpStatus to numeric 502, then constructs the JSON payload {error: "DEPENDENCY_ROUTE_FAILED"}. Inspect the Listener response status expression vars.httpStatus default 200 as well. Continue selects the normal response path, so that variable must carry the intended failure status.

The shipping service returns 503. Scatter-Gather surfaces a composite routing failure, which this flow translates to status 502 and DEPENDENCY_ROUTE_FAILED. A successful customer lookup does not make the combined response successful.

A composite error can carry information about successful and failed routes. Use that information deliberately if the contract supports partial results; do not turn it into an accidental success by dropping the failed route. Equally, propagating the composite error does not undo an external write that another route already completed.

For this reason the example performs reads — splitting order creation and notification into two parallel routes would create a distributed partial-failure problem. Chapter 19 connects notification intent to the order transaction instead.

Time and capacity still matter

For independent calls, elapsed waiting is closer to the slowest route than the sum of all route times, plus scheduling and combination overhead. That is a reasoning model, not a benchmark for this laptop. A slow route can still dominate the request, and waiting for all routes can retain memory and connections.

The budget worth writing down starts from the caller’s deadline and allocates time to the required dependencies and to building the response. A timeout on one requester does not automatically cancel already completed work elsewhere — it stops this side waiting, not the other side working. Load tests need the actual latency distribution, concurrency and destination quotas.

Try it

1. Read the joined shape. Why does the customer selector contain two uses of payload?

Show answer

The first identifies the event payload containing route results. The second selects the payload of the Message stored for route zero. The result is then the customer object whose name is Dana.

2. Count concurrent calls. Ten requests each run Parallel For Each with maximum concurrency two. Is the dependency protected by a global limit of two?

Show answer

No. Each scope owns its own limit. The aggregate can be much higher, subject to other pools and scheduling. Protect a shared dependency with a capacity plan that covers all callers.

3. Replace a read with a write. If one route commits an order and the other fails, what does Propagate establish?

Show answer

The owning flow fails. It does not establish rollback of the remote committed order. Retain the remote identity and reconcile or compensate according to the business contract.

Parallelism spends capacity to reduce waiting. Retry spends additional attempts to recover from failure. Both need a clear account of which effects may already have happened.

Next: Repeat a Request Safely

Comments