← Wiki

Correlation Identifier in Mule: the book's pattern and the correlationId

  • 15 min read
On this page

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 sends Request 1 (request ID = 1) and Request 2 (request ID = 2) to the Replier. The Reply with correlation ID = 2 arrives first and is matched to request 2, then the one with correlation ID = 1 is matched to request 1.

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 Payment sub-flow stores the request by requestId in the Pending requests Object Store (1-hour TTL), sets the order to PAYMENT_REQUESTED in the Status Object Store and sends POST /eip/provider/payments to the Simulated provider, which answers 202 Accepted. Later, in an async scope, the provider sends POST /eip/payments/callback with correlationId = requestId, status and approvedAmount to the Callback flow, which reads and removes the pending request. With no pending request it answers 404 and the order does not change. When the request is found, the order goes to PAID (APPROVED with the requested amount), PAYMENT_DECLINED (DECLINED) or PAYMENT_REVIEW with the reason (another amount or status), and the answer is 200 with result MATCHED and the orderStatus.

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 for ORD-CID-A (100) and ORD-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 up PAID with its own providerReference and 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 the async scope, and the test reads the status store with os:retrieve, inside an until-successful, until order ORD-CID-E2E is PAID, with amount 42.
  • cid-request-carries-its-id-and-not-the-order. The body sent to the provider has only the requestId and amount keys, and the pending request keeps the orderId.

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.

The Client sends a request, with or without X-CORRELATION-ID, to Application A, which adopts the header or generates a correlationId. With Send Correlation Id set to AUTO (default), Application A sends the HTTP Request with X-CORRELATION-ID, Application B adopts the same correlationId and the logs of both show the same ID. With NEVER, the HTTP Request goes without the header and Application B generates a new correlationId.

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:retrieve and os:remove are two calls, not a lock. Whether the second of two simultaneous copies fails on os:remove depends 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 in PAYMENT_REQUESTED until 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:

  1. POST /eip/payments/after/charges with { "orderId": "ORD-2001", "amount": 100 } charges the order and requests the payment from the simulated provider.
  2. GET /eip/payments/ORD-2001/status shows the outcome stored by the callback: with the simulated provider, PAID, with requestId, requestedAmount, providerStatus, approvedAmount and providerReference.
  3. POST /eip/payments/callback with correlationId PRQ-UNKNOWN gets 404.
  4. GET /eip/correlation-id/call/default and /call/never return 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.

Code for this page