Unveiling the Enterprise Integration Patterns (EIP) - Part Three

software development

January 2025.
Vladimir Vasić's profile image

Vladimir Vasić

Team Lead

Hey there! In my previous post, we dove into the fundamentals of EIP - Messages and message-passing between components. Today, I'm excited to share how we've actually implemented some more advanced patterns in our demo project, specifically around message routing and transformation. Let me show you how we put these concepts into practice and what we learned along the way.

From Theory to Practice: Our Demo Project Journey

The Patterns Behind Our Implementation

One of the biggest challenges we faced in our demo project was providing a way to easily add new business workflows within our core component. We needed these workflows to be isolated, each with its own channel for requests. Here’s where it gets interesting - we had multiple publishers sending messages to our entry point (a DB-backed queue with a polling consumer), and we needed a smart way to direct these messages to the right workflow. After some experimentation, we found that the Content-Based Router pattern was perfect for our needs. Let me show you how we implemented it using Spring Integration:

@Bean
fun <T : Any> workflowRouterFlow(): IntegrationFlow = integrationFlow(partnerRouterChannel()) {
    routeToRecipients {
        recipientFlow<Notification<T>>({ it.flow == Workflow.Example }, {
            enrichHeaders(headerExpression(
                "exampleField",
                "payload.nestedField.exampleField"
            ))
        })
        channel(exampleWorfklowChannel)
    }
}

We could have gone with a publish/subscribe system instead, but I found that would’ve been less scalable since each message would need to be sent to every workflow channel. The Content-Based Router offers much better performance characteristics.

Looking at this snippet, I implemented a content-based router that examines the message’s flow field to figure out where it needs to go. It’s pretty straightforward - if it finds a matching value, it routes the message to that workflow’s channel. What I found particularly neat was how we could layer multiple patterns in our core component. We’re not just routing - we’re also using Message Transformation patterns. I went with the Envelope Wrapper pattern by creating a Notification object (which wraps our flow field), and added some Content Enricher functionality while we were at it. The Notification wrapper turned out to be a really clean way to handle all this - it’s basically like putting our message in a smart envelope that knows where it needs to go. I can show you how the Notification class looks if you’re interested in the implementation details!

Message Transformation Patterns

In our core component, we implemented both the Envelope Wrapper and Content Enricher patterns. Here’s our Notification wrapper class:

data class Notification<T>(
    val flow: Workflow,
    val body: T,
    var headers: MutableMap<String, Any> = mutableMapOf()
) : Serializable

I particularly love how this simple wrapper helps us standardize message handling across the system. It’s been a game-changer for managing message persistence in our DB queue and handling system-wide metadata.

Content Enricher Pattern

We’ve actually started implementing this in our latest sprint. It’s perfect for cases where we need to fetch additional data from external systems. Here’s how we’re using it:

enrichHeaders(headerExpression(
    "exampleField",
    "payload.nestedField.exampleField"
))

I discovered we could leverage Spring Integration’s SpEL (Spring Expression Language) expressions to do some pretty clever things. This pattern isn’t just about copying fields around - we can actually transform data on the fly! For example, we could enrich our messages with:

  • Calculated values based on the original payload
  • Environment-specific configurations
  • Data retrieved from external services
  • Timestamp and audit information

One particularly useful trick I learned was using this pattern to standardize data formats across different systems. Instead of forcing every publisher to conform to a specific format, we can handle the transformation right at the routing level. This really helped reduce component coupling.

As always, Documentation!

Before diving into all these patterns and implementations, there’s something we should establish right at the start - good documentation practices! When working with Enterprise Integration Patterns, keeping clear and up-to-date documentation is essential. Let me walk you through the key patterns we’ll be exploring and how we can effectively document them in our systems.

The Building Blocks: Pipes and Filters

Description

Pipes and Filters enable message transformation through several composable steps. Each of these steps takes a message as input, performs transformations on it, and finally passes the result to the next step. These steps are called filters, and the channels between them are called pipes. The most important property of this pattern is the ability to freely rearrange, add, and remove filters since they all share the same generic interface. A step could be a part of the same component, or running in a different environment altogether.

image 1

Usage

Pipes and filters are used when the process of transforming a message needs to be broken down into a series of composable steps.

Example

When implementing a REST API that deals with user-uploaded images, we need to perform different transformations and checks, i.e. content moderation or removing user private data. To model this, a series of pipes and filters could be used, where each filter would accept the uploaded image as the input and return the transformed image as the output.

image 2

Smart Dispatching: Journey into Message Routing

Description

Message router expands on the Pipes and Filters pattern by having multiple output channels, thus considering the case of non-sequential message processing. It is a new filter that takes a message from a channel, evaluates a set of predicates, and decides to which of the predefined message channels it will publish the message. The publishers of the original message remain unaware of the routing logic, which achieves loose coupling between the components.

Usage

Used when multiple publishers are sending messages to the same channel, and we want to keep routing logic out of the publishers.

Example

When implementing a Message Broker, we usually want to route incoming messages to different consumer subscriptions.

image 3

Content-Based Router in Detail

Description

Content-Based router is a variation of the Message Router in which the payload of the message itself is examined to decide which message channel to publish the message to. The criteria for the decision could be the payload format, or the values of certain fields inside the payload.

Usage

Used when there is a need to make a routing decision based on the payload of the incoming message.

Example

When implementing an application for importing bulk data, we want to route the incoming message by examining if the underlying format of the content is CSV or JSON.

image 4

Multi-Stop Delivery: The Recipient List Pattern

Description

Recipient list is an extension to the Message Router pattern in which the message is published to multiple channels instead of one after a routing decision has been made. Each recipient has its own channel. The list of the recipients is computed inside the router component, or fetched externally. In order to avoid increased coupling, the publishers of the original message must remain oblivious to the logic of making routing decisions, as well as how the list is being computed.

Usage

Used when there is a need to make a routing decision, usually based on the payload of the message, and to publish the message to multiple subscribers.

Example

When implementing a service for making loans that forwards the request to a set of different banks, we need to decide which bank is eligible for the type of loan requested. We examine the message payload and forward the request accordingly.

image 5

Message Makeovers: The Art of Transformation

Think of Message Channels and Message Routers as postal workers who don’t care which city or office your message comes from or goes to - they just know how to get it there. But here’s where Message Transformation comes in with a different kind of magic: it’s all about making sure the actual content of your message makes sense to whoever receives it! What I love about using these transformation patterns is how they let our components stay blissfully unaware of each other’s specific needs. It’s like having a universal translator - each component can speak its own language, and the transformation layer handles all the conversions. We can transform messages in a few clever ways:

  • Wrap them in a format our messaging system understands (like putting a letter in the right kind of envelope).
  • Add extra information that might be needed (either calculated from what we have or fetched from somewhere else).
  • Remove any unnecessary parts to avoid confusing the receiver.

This approach has been a game-changer for keeping our components loosely coupled. Each part of the system can focus on its job without worrying about how other components expect their data to be formatted.

The Perfect Fit: Envelope Wrapping Pattern

Description

The Envelope Wrapper pattern wraps the contents of a message in a wrapper and unwraps it at the destination.

Usage

Used when working with a messaging system with specific message format requirements, preserving the format and the original contents for later processing.

Example

A TCP/IP packet is transmitted over a network.

image 6

The Detail Painter: Content Enrichment in Action

Description

The Content Enricher pattern is used to add additional data to the message headers or body when the target system requires it. The data can be fetched from the environment, another external system, or computed from the already existing contents of the message.

Usage

Used when the target system requires additional data to process the incoming message.

Example

When implementing a system that processes customer orders, the address for a specific customer might be stored in a different system to comply with regulatory requirements. After receiving a request for a new order, the address must be fetched from the external system in order to process the shipping.

image 7

The Information Distiller: Content Filter Pattern

Description

The Content Filter pattern is similar to the Content Enricher, except that it removes extraneous content from the message. This is useful when the message is too big, or when most of the data fields are not used by the target system.

Usage

Used when the target system requires less data than the message payload contains.

Example

When a response is received from an external system, it might contain nested fields that the target system is interested in. The Content Filter pattern is used to extract only the relevant fields and flatten the structure of the message payload.

What’s Next?

While I’m really happy with how these patterns have helped us decouple our components, I’ve noticed they can make debugging and error handling more complex. In my next post, I’ll share our experiences with handling these operational challenges, exploring patterns like Detour, WireTap, Message History, and Message Store. I’d love to hear about your experiences with these patterns in the comments below!

As Porky Pig would say, “Th-th-th-that’s all folks!” … For now! Stay tuned, and happy coding!

Share