Last time around, I did a little work with service composition in hopes to come up with an approach I could use in a real-world project. The approach I came up with last time appeared to fit the bill, until another requirement was added: The response from the first service would be needed to make the request to the second.
The new requirement definitely forced me to rethink my previous approach, as I could no longer simply send the intial request off to be routed to the two services that needed to be consumed, aggregating the responses for a request to the business logic.
For this example, I'll try to come up with a similar problem for a service to solve that follows a similar processing flow: The goal of this new service, given an order number, is to respond with a report about the customer containing their information and order history.
In order to accomplish this task, this new service will need to make use of two remote services:
- A customer service: Returns the information for a customer who placed a particular order.
- An order history service: Returns the order history for a particular customer.
The information contained in both service responses is used in the business logic contained within this composite service to produce a report, let's say to relate the ordering of certain products to different demographic profiles.
Now at this point, one might say: "Why can't the order history service return the customer information its provided in its request in its response?".
A good question, but some would argue that a service should not respond with data the caller already has or knows. My job here would be a lot of easier if the order history service could be changed to do just that, I could just pass the order history response to my business logic and not need to keep the customer service response around. On the other hand, the order history service should just be charged with returning the information it was requested to produce and not concern itself with how its response will ultimately be used, as long as that response is usable and correct. Not all clients might make use of the customer information returned in the order history response, and we should not request that type of overhead.
Ok, enough talking and more Mule-ing. For now I can start with the two things I know for certain, I will need to make requests to both the customer service and order history service, and they need to happen in order, so let's start there. For the sake of brevity, I'll omit any needed transformations to make the processing paths easier to follow.
<service name="orderService">
<inbound>
<jms:inbound-endpoint queue="order.queue" synchronous="true"/>
</inbound>
<outbound>
<chaining-router>
<vm:outbound-endpoint path="customer.queue" synchronous="true"/>
<vm:outbound-endpoint path="history.queue" synchronous="true"/>
</chaining-router>
</outbound>
</service>
The incoming request will be routed to a VM queue where a listening internal service will route it to the remote customer service:
<service name="customerService">
<inbound>
<vm:inbound-endpoint path="customer.queue"/>
</inbound>
<outbound>
<pass-through-router>
<jms:outbound-endpoint queue="customer.service.request.queue" synchronous="true"/>
</pass-through-router>
</outbound>
</service>
The response from the customer service will be provided to the order history service, which is defined similarly.
Now the next problem here is getting both responses to the same place so a request for my business logic can be created and the request carried out. From here, I'll add the internal service containing the business logic:
<service name="reportService">
<inbound>
<vm:inbound-endpoint path="responses.queue" synchronous="true"/>
<custom-inbound-router class="ResponseAggregator"/>
</inbound>
<component>
<spring-object bean="reportingComponent"/>
</component>
</service>
The inbound router is an extension of Mule's AbstractEventAggregator class that waits for a group of 2 responses (to avoid a context switch I'll omit the implementation details for now). It's job is to take both service responses and create a request for the reportingComponent. Now that we have that defined, how do we get the service responses to its VM queue?
Getting the order history response to the queue is easy, we just add another outbound endpoint to the chaining router:
<service name="orderService">
<inbound>
<jms:inbound-endpoint queue="order.queue" synchronous="true"/>
</inbound>
<outbound>
<chaining-router>
<vm:outbound-endpoint path="customer.queue" synchronous="true"/>
<vm:outbound-endpoint path="history.queue" synchronous="true"/>
<vm:outbound-endpoint path="responses.queue" synchronous="true"/>
</chaining-router>
</outbound>
</service>
Now we need to get the customer service response to the order history service and to the same response.queue. A "customer response routing service" might work here. This new service would be added as an endpoint in the chaining router between the routings to the customer and history services. This new internal service needs to route its request to the VM queue for the report service and return the request to the chaining router:
<service name="customerRoutingService">
<inbound>
<vm:inbound-endpoint path="customer.routing.queue" synchronous="false"/>
</inbound>
<outbound>
<multicasting-router>
<vm:outbound-endpoint path="responses.queue" synchronous="false"/>
<vm:outbound-endpoint path="async.queue" synchronous="false"/>
</multicasting-router>
</outbound>
<async-reply>
<vm:inbound-endpoint path="async.queue"/>
<single-async-reply-router>
<!-- we want to return the message that maps to the correlation ID found on the original request -->
<expression-message-info-mapping messageIdExpression="#[header:JMSCorrelationID]"
correlationIdExpression="#[header:JMSCorrelationID]"/>
</single-async-reply-router>
</async-reply>
</service>
Unlike the other internal services that respond synchronously, this service is asynchronous. To the caller, the chaining router, the request will have been handled synchronously, following the Async Request Response style.
The request received (the customer information), will be sent to the desired VM queue and to the async.queue where a reply router will return the customer information response it recevied on its inbound endpoint to the chaining router.
Now we place the routing to the customerRoutingService into the chain:
<service name="orderService">
<inbound>
<jms:inbound-endpoint queue="order.queue" synchronous="true"/>
</inbound>
<outbound>
<chaining-router>
<vm:outbound-endpoint path="customer.queue" synchronous="true"/>
<vm:outbound-endpoint path="customer.routing.queue" synchronous="true"/>
<vm:outbound-endpoint path="history.queue" synchronous="true"/>
<vm:outbound-endpoint path="responses.queue" synchronous="true"/>
</chaining-router>
</outbound>
</service>
Since the last endpoint in the chain will be the second message placed on responses.queue, the inbound router for the reportService will be invoked, in turn invoking the component. The response from the component will be returned to the chain, completing it. The chaining router will then return the response to the caller of the orderService.
To sum up the processing flow:
- A request is sent to the order service.
- The request is forwarded to the remote customer service.
- The customer service response is sent to the customer response routing service.
- The routing service sends the request to the responses queue (event #1) and to an async reply queue.
- A listener on the async reply queue returns the customer service response to the chaining router
- The customer service response is forwarded to the remote order history service.
- The order history response is sent to the responses queue (event #2).
- The aggreagation router listening to the responses queue, having both events, is invoked, creating the reporting request.
- The reporting component is invoked creating the service response.
- The response is returned to the chaining router, completing the chain.
- The response is returned to the caller.
This could one of several approaches to the stated problem, alternatives are welcome!