Correlation Identifier in Mule: the book's pattern and the correlationId
- 15 min read
On this page
- The provider replies later: which request is this callback for?
- The pattern in the book: the request’s ID comes back in the reply
- Diagram: the pattern in the book
- The book’s pattern in Mule: the payment provider’s callback
- Diagram: the payment confirmed by callback
- The request carries the requestId, not the orderId
- The simulated provider answers 202 and calls back
- The callback claims the pending request
- Out-of-order callbacks, in the tests
- Mule’s correlationId: the same ID in the logs of both applications
- Diagram: X-CORRELATION-ID between two applications
- The two meanings side by side
- Things to watch with each ID
- When the book’s pattern is too much: the reply on the same HTTP call
- Running the example and the tests
Code
In Mule 4, “correlation ID” has two meanings. One is the Correlation Identifier pattern from the book Enterprise Integration Patterns, which links a reply to the request that triggered it. The other is Mule’s correlationId, which follows a request from one application to the next. This page shows both in the series scenario, a charged order that the payment provider confirms later, with the code and MUnit suites from the mulesoft-patterns-examples repository.
The provider replies later: which request is this callback for?
The book starts from an application that made a request through messages and received a reply, and asks how it knows which request that reply belongs to (Correlation Identifier).
In the example, after charging an order, the payment consumer requests the payment from a provider, which accepts with 202 and, later, calls a callback endpoint to say whether it approved or declined it. Other orders were sent in the meantime, and the callbacks can arrive in any order. The consumer needs to work out which order each one belongs to.
The pattern in the book: the request’s ID comes back in the reply
The book’s solution is for each reply to carry a correlation identifier, a unique ID that indicates which request it answers. The pattern has six parts:
- the requestor, the application that sends the request and waits for the reply;
- the replier, the application that receives the request, handles it and replies;
- the request;
- the reply;
- the request ID, which travels in the request and identifies it uniquely;
- the correlation ID, which travels in the reply with the same value as the request ID, copied by the replier.
Diagram: the pattern in the book
The diagram answers how the requestor matches each reply to its request when the replies come back out of order.
The requestor keeps the requests that are waiting for a reply and looks up each reply’s correlation ID among them. Arrival order is no clue: nothing guarantees that the first reply belongs to the first request.
The book’s pattern in Mule: the payment provider’s callback
Mule has no component for this pattern. In the example, it is built with HTTP Request, HTTP Listener and two object stores, in correlation-identifier.xml. After the idempotent receiver charge, the payment-request-from-provider sub-flow plays the requestor. The replier is the payment-provider-simulator flow, a simulated provider inside the same application.
Diagram: the payment confirmed by callback
The diagram answers where the requestId travels, from the charge to the outcome stored on the order, and what happens to a callback that no request is waiting for.
The request carries the requestId, not the orderId
The sub-flow generates a requestId ("PRQ-" ++ uuid()) and sends the provider only requestId and amount: the provider has no reason to know the application’s orders. Before the call, it stores the whole request, with the orderId and the amount, in the pending store, with the requestId as the key, because the callback can arrive before the 202 does.
<!-- Pending requests by requestId. TTL 1 hour: a callback later than that is treated as unknown. -->
<os:object-store name="eip-pending-payment-requests-store" doc:name="Pending payment requests"
persistent="false" entryTtl="1" entryTtlUnit="HOURS" expirationInterval="5" expirationIntervalUnit="MINUTES" />
In the repository: src/main/mule/eip/correlation-identifier/correlation-identifier.xml
<!-- Stored before the call: the callback can arrive before the provider's 202 does. -->
<os:store doc:name="Remember pending request" key="#[vars.paymentRequest.requestId]" objectStore="eip-pending-payment-requests-store">
<os:value><![CDATA[#[vars.paymentRequest]]]></os:value>
</os:store>
<os:store doc:name="Order is waiting for payment" key="#[vars.paymentRequest.orderId]" objectStore="eip-order-payment-status-store">
<os:value><![CDATA[#[output application/java --- { orderId: vars.paymentRequest.orderId, status: "PAYMENT_REQUESTED", requestId: vars.paymentRequest.requestId }]]]></os:value>
</os:store>
<!-- The order is kept aside and put back after the call, so the caller still has it as payload. -->
<set-variable doc:name="Keep the order" variableName="chargedOrder" value="#[payload]" />
<http:request doc:name="Send payment request to provider" config-ref="eip-self-http-request-config" method="POST" path="/eip/provider/payments">
<http:body><![CDATA[#[output application/json --- vars.providerRequestBody]]]></http:body>
</http:request>
<set-payload doc:name="Order back as payload" value="#[vars.chargedOrder]" />
The order goes into a variable and comes back as the payload after the call, because the charge flow keeps using the order.
The simulated provider answers 202 and calls back
The provider answers 202 and, inside an async scope, calls the path configured in eip.provider.callbackPath, the way a real provider calls the webhook registered with it. The main flow does not wait for the async scope to finish (Async), so the 202 does not depend on the callback, which returns the requestId in the correlationId field:
<set-variable doc:name="Provider request" variableName="providerRequest" value="#[payload]" />
<async doc:name="Approve later">
<http:request doc:name="Call back the requestor" config-ref="eip-self-http-request-config" method="POST" path="${eip.provider.callbackPath}">
<http:body><![CDATA[#[output application/json --- {
correlationId: vars.providerRequest.requestId,
status: "APPROVED",
approvedAmount: vars.providerRequest.amount,
providerReference: "PRV-" ++ uuid()
}]]]></http:body>
</http:request>
</async>
<set-payload doc:name="Accepted" value="#[output application/json --- { requestId: vars.providerRequest.requestId, status: 'ACCEPTED' }]" />
In the repository: src/main/mule/eip/correlation-identifier/correlation-identifier.xml
The simulated provider always approves the requested amount. The other outcomes appear in the tests that send the callback themselves.
The callback claims the pending request
The payment-confirm-from-provider-callback flow uses only the correlationId in the body. Without a correlationId, it answers 400. With one, it reads the pending request and removes it from the store before touching the order: that is how the callback claims the request. If the key does not exist, os:retrieve or os:remove throws OS:KEY_NOT_FOUND (Object Store reference), and the try scope turns that error into a 404. The reason may be an unknown ID, an expired entry, a callback already matched earlier, or another copy that removed the request between this one’s os:retrieve and os:remove.
<set-variable doc:name="Callback" variableName="callback" value="#[payload]" />
<set-variable doc:name="Not matched yet" variableName="unmatched" value="#[false]" />
<try doc:name="Match and claim the pending request">
<os:retrieve doc:name="Find pending request" key="#[vars.callback.correlationId]" objectStore="eip-pending-payment-requests-store" target="pendingRequest" />
<os:remove doc:name="Claim the pending request" key="#[vars.callback.correlationId]" objectStore="eip-pending-payment-requests-store" />
<error-handler>
<!-- Unknown, expired, already matched, or claimed by another copy in between: all the same 404. -->
<on-error-continue doc:name="No pending request" type="OS:KEY_NOT_FOUND" logException="false">
<set-variable doc:name="Not matched" variableName="unmatched" value="#[true]" />
</on-error-continue>
</error-handler>
</try>
<choice doc:name="Matched?">
<when expression="#[vars.unmatched default false]">
<logger doc:name="Log unmatched callback" level="WARN" message="#['Unmatched provider callback, correlationId in body=' ++ vars.callback.correlationId ++ ', correlationId=' ++ correlationId]" />
<raise-error doc:name="Unknown correlationId" type="APP:NOT_FOUND" description="No pending payment request for this correlationId" />
</when>
</choice>
In the repository: src/main/mule/eip/correlation-identifier/correlation-identifier.xml
With the request in hand, the flow decides the outcome from the callback’s status and the approved amount:
<choice doc:name="Route on callback status">
<when expression="#[vars.callback.status == 'APPROVED' and vars.callback.approvedAmount == vars.pendingRequest.amount]">
<set-variable doc:name="Paid" variableName="outcome" value="#[{ status: 'PAID' }]" />
</when>
<when expression="#[vars.callback.status == 'DECLINED']">
<set-variable doc:name="Declined" variableName="outcome" value="#[{ status: 'PAYMENT_DECLINED' }]" />
</when>
<when expression="#[vars.callback.status == 'APPROVED']">
<set-variable doc:name="Review: amount mismatch" variableName="outcome" value="#[{ status: 'PAYMENT_REVIEW', reviewReason: 'AMOUNT_MISMATCH' }]" />
</when>
<otherwise>
<set-variable doc:name="Review: unknown status" variableName="outcome" value="#[{ status: 'PAYMENT_REVIEW', reviewReason: 'UNKNOWN_STATUS' }]" />
</otherwise>
</choice>
The flow writes to the status store the outcome, the review reason when there is one, the requestId, the requested amount (requestedAmount) and, from the provider, the status (providerStatus), the approvedAmount and the providerReference. The status can be queried at GET /eip/payments/{orderId}/status. All three outcomes answer 200: the provider has already given its final answer, and rejecting the callback would only make it send the same answer again.
| Callback | Response | Order | Test |
|---|---|---|---|
APPROVED with the requested amount |
200 { result: MATCHED, orderStatus: PAID } |
PAID |
cid-order-is-paid-by-the-provider-callback, cid-out-of-order-callbacks-are-matched-to-their-orders |
DECLINED |
200, orderStatus: PAYMENT_DECLINED |
PAYMENT_DECLINED |
cid-declined-callback-marks-the-order-payment-declined |
APPROVED with another amount (90 instead of 100) |
200, orderStatus: PAYMENT_REVIEW |
PAYMENT_REVIEW, reviewReason: AMOUNT_MISMATCH |
cid-approved-callback-for-another-amount-goes-to-payment-review |
Another status (CHARGEBACK) |
200, orderStatus: PAYMENT_REVIEW |
PAYMENT_REVIEW, reviewReason: UNKNOWN_STATUS |
cid-callback-with-unknown-status-goes-to-payment-review |
Repeat of an already matched callback, or a late DECLINED |
404 |
Keeps the first outcome | cid-repeated-callback-returns-404-and-keeps-the-first-outcome |
Copy that loses the request between os:retrieve and os:remove |
404 |
Unchanged | cid-callback-that-loses-the-claim-returns-404-and-changes-nothing |
Unknown correlationId |
404 |
No order | cid-unmatched-callback-returns-404 |
No correlationId |
400 |
No order | cid-callback-without-correlation-id-returns-400 |
In the test for the copy that loses the request, a mock makes os:remove throw OS:KEY_NOT_FOUND, as if another copy had removed the key first; the order stays in PAYMENT_REQUESTED. The example does not check who sent the callback. In a real webhook, also verify the provider’s signature before trusting the body.
Because everything runs in the same application, Mule’s X-CORRELATION-ID header also arrives with the callback. A real provider does not return it, and the flow does not read it: the match depends only on the correlationId in the body.
Out-of-order callbacks, in the tests
The correlation-identifier-test-suite.xml suite has 14 tests, which pass on Mule 4.9.17 with MUnit 3.7.1. Besides the ones in the table above:
cid-out-of-order-callbacks-are-matched-to-their-orders. Requests payment forORD-CID-A(100) andORD-CID-B(250) with the provider replaced by a mock that never calls back, and the test itself sends the callbacks, B before A. Each order ends upPAIDwith its ownproviderReferenceand its own amount, and both requests leave the pending store.cid-order-is-paid-by-the-provider-callback. End to end: the simulated provider calls back on its own, from theasyncscope, and the test reads the status store withos:retrieve, inside anuntil-successful, until orderORD-CID-E2EisPAID, with amount 42.cid-request-carries-its-id-and-not-the-order. The body sent to the provider has only therequestIdandamountkeys, and the pending request keeps theorderId.
Mule’s correlationId: the same ID in the logs of both applications
Every Mule event has a correlationId, which ties log lines to one execution. Mule first looks for one at the source (for example, in a JMS message or, on the HTTP Listener, in the X-CORRELATION-ID header) and generates a new one only when none comes in (Correlation ID). HTTP Request has the Send Correlation Id option, with AUTO, ALWAYS and NEVER, and AUTO as the default (HTTP Connector).
On Mule 4.9.17, the test mule-cid-http-request-sends-the-caller-correlation-id shows that, with the option at its default, the called flow receives the caller’s ID in X-CORRELATION-ID and adopts it. So when an application A calls an application B with the option at its default, B adopts A’s ID, because it is also a Mule application and receives the call on an HTTP Listener. A search for that ID in the logs brings up the request in both applications.
Diagram: X-CORRELATION-ID between two applications
The diagram answers where the called application’s correlationId comes from, with the option at its default and with NEVER.
In mule-correlation-id.xml, the called flow returns the header it received and its event’s correlationId:
<flow name="mule-correlation-id-echo">
<http:listener doc:name="GET /eip/correlation-id/echo" config-ref="http-listener-config" path="/eip/correlation-id/echo" allowedMethods="GET" />
<ee:transform doc:name="What arrived">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{ receivedHeader: attributes.headers["x-correlation-id"], calleeCorrelationId: correlationId }]]></ee:set-payload>
</ee:message>
</ee:transform>
</flow>
In the repository: src/main/mule/eip/correlation-identifier/mule-correlation-id.xml
The flows at /eip/correlation-id/call/default and /call/never call it, and the two calls differ only in sendCorrelationId:
<http:request doc:name="Call echo (default)" config-ref="eip-self-http-request-config" method="GET" path="/eip/correlation-id/echo" />
<http:request doc:name="Call echo (NEVER)" config-ref="eip-self-http-request-config" method="GET" path="/eip/correlation-id/echo" sendCorrelationId="NEVER" />
The test mule-cid-send-correlation-id-never-does-not-send-it confirms that, with NEVER, the header does not arrive and the called flow gets a different ID. The calls in both tests are real HTTP, but within the same application: HTTP Request points to localhost, on the application’s own port. Between two applications the mechanism is the same header; the test just does not cross the network between two deployed applications.
To change the ID for part of a flow, the documentation points to the with-correlation-id scope from the Mule Tracing module (Correlation ID).
The two meanings side by side
| Correlation Identifier (book) | correlationId (Mule) |
|
|---|---|---|
| What it is for | Linking a reply to its request | Following an execution through the logs, across applications |
| Who generates it | The requestor; the replier only copies it | Mule, when the source does not bring one |
| How it travels | In the request and the reply (in the example, in the JSON body) | Over HTTP, in the X-CORRELATION-ID header; with other sources, in their own correlation field |
| Question it answers | Which request is this reply for? | Where did this request go? |
Things to watch with each ID
Mule’s correlationId does not identify a business message. Calls that leave the same execution carry the same ID, and a client that resends without the header arrives with a new ID. That is why it does not work as an idempotency key, and it is exactly the ID the Idempotent Message Validator uses by default; the idempotent receiver page shows both errors in tests.
The pending request expires. The pending store has a 1-hour TTL (entryTtl) and checks for expired entries every 5 minutes (expirationInterval). According to the Object Store reference, entries are removed by the expiration thread, which runs at that interval; so an entry leaves at the first check after it expires, not exactly when the hour is up. Once it is gone, the callback gets 404, like an unknown ID, and the order stays in PAYMENT_REQUESTED, because the status store has no TTL. No test covers expiration: this comes from the configuration and the code.
Claiming before updating narrows the race, but does not close it. The flow removes the pending request before writing the outcome to the order, and the tests show the two expected consequences: a repeated callback gets 404 and the order keeps the first outcome; a copy that loses the request between os:retrieve and os:remove gets 404 and changes nothing. Two limits remain, which come from the code and have no test:
os:retrieveandos:removeare two calls, not a lock. Whether the second of two simultaneous copies fails onos:removedepends on the store. With persistent stores on CloudHub, which use Object Store v2, there is no distributed locking, and the documentation recommends an external lock when several components update the same store at the same time (Object Store v2). So, with persistent stores, two copies handled at the same instant, by two workers, can still both update the order. To rule this out entirely, you need an atomic claim or a lock outside this flow.- If writing the outcome to the order fails after the claim, the request has already left the pending store: the provider’s retry gets
404, and the order stays inPAYMENT_REQUESTEDuntil someone checks.
The stores are in memory (persistent="false"), so each run starts clean. A restart wipes the pending requests, and their callbacks get 404; this comes from the code, with no test. An in-memory store is also not shared between replicas: with two workers outside a Mule cluster, the callback may reach the worker that did not store the request and get 404. In a Mule high-availability cluster, object stores, persistent or not, live in a memory grid shared among the nodes (HA clusters). Outside a cluster, the repository README points to a persistent, shared store, such as Object Store v2 on CloudHub, with the lock limit from the previous item.
When the book’s pattern is too much: the reply on the same HTTP call
The book describes two ways to receive the reply (Request-Reply). With synchronous blocking, the requestor sends and waits, which assumes a single outstanding request or a dedicated reply channel. With an asynchronous callback, several outstanding requests share the reply channel, and that is where the Correlation Identifier comes in.
A synchronous HTTP call falls into the first case: HTTP Request waits for the response on the same connection, which is already tied to the request. If the provider returned the approval in the body of the call itself, a request ID kept in a store would only add moving parts. The pattern becomes necessary when the reply arrives by another route: a callback, a reply queue, a return file.
Running the example and the tests
Requirements: Java 17, Maven 3.9 or newer, and the MuleSoft Enterprise repository credentials in ~/.m2/settings.xml, because MUnit runs on the Enterprise runtime.
git clone https://github.com/brunosouzas/mulesoft-patterns-examples.git
cd mulesoft-patterns-examples
mvn clean test
To call the endpoints, run the application in Anypoint Studio (port 8081) and open requests.http in VS Code’s REST Client or in IntelliJ:
POST /eip/payments/after/chargeswith{ "orderId": "ORD-2001", "amount": 100 }charges the order and requests the payment from the simulated provider.GET /eip/payments/ORD-2001/statusshows the outcome stored by the callback: with the simulated provider,PAID, withrequestId,requestedAmount,providerStatus,approvedAmountandproviderReference.POST /eip/payments/callbackwithcorrelationIdPRQ-UNKNOWNgets404.GET /eip/correlation-id/call/defaultand/call/neverreturn the caller’s ID alongside the header received and the called flow’s ID.
The other patterns in the series, with the Mule component for each one, are in the catalog of integration patterns in Mule.