Picture cool kids in startups, cranking code as if their lives depend on it, focusing on the proverbial MVP above all else. At this stage, who cares if technical debt accumulates as fast as code gets written? It would be a waste of time and focus to try to keep the field as green as it was initially. Then the worst happens: the cool kids have it right, people love their new app and traffic starts to surge. Though strong, the duct tape that olds the application together starts to show signs of fatigue. Maintenance becomes painful, adding new features is excruciating. The blood of the architecture that has been sacrificed on the altar of time-to-market is calling for revenge.
One of the most typical architectural mishap that comes back to haunt startups is tight coupling: the whole system is a monolith where coupling manifests itself both from a temporal manner (everything is synchronous) and a lack of abstraction in the interactions between the subsystems (everything knows the intimate details of everything else).
The good news is that there is hope: the giants of past time, upon whose shoulders everything is built, have fought these problems and won. Take Hohpe/Woolf’s Enterprise Integration Patterns (EIP) for example. They discuss how messaging can be used to alleviate coupling issues. Sure enough, the “enterprise” term in the name is dubbed “run away!” by our startups’ cool kids. So in this post we’ll look at a few of these patterns and how they could be used beneficially in modern applications. And hopefully these patterns will feel more lovely than enterprisey!
There are 65 patterns in the EIP catalog. We’ll cover just a handful while exploring different scenarios that typical web applications face.
With the advent of streaming APIs, whether based on long polling or web sockets, applications have the need to be able to reach established connections and deliver messages to them. Suppose you have multiple web servers with established web sockets with client browsers, how do you send a message to a single web socket ? To a group of them? To all of them?
Using a publish/subscribe messaging pattern allows to implement such a behavior: when a web socket gets established, the corresponding server-side entity can subscribe to different topics in order to receive global, group or personal messages and route them back to the socket. Whether using a messaging broker (for example with JMS or AMQP), a broker-less approach (like with ØMQ) or a distributed actor or process model (like with Akka or Erlang), the publish-subscribe pattern allows reaching out to decoupled and distributed systems.
Not all inbound requests are born equal. Some need to be processed synchronously but some can be processed later. Deciding to decouple the acceptance of a request from its processing can be the single difference between a system that scales and one that doesn’t. Moreover, introducing such decoupling allows scaling the processing components independently from the request-accepting ones.
The channel adapter pattern is what we’re after here: with this pattern we could be accepting HTTP requests and adapt them into messages ready to be sent to a messaging channel. Another pattern we also rely on is guaranteed delivery. Indeed, once a message has been accepted and sent to a messaging channel, we don’t want to lose it! Having a strong guarantee that it will eventually be delivered to the intended recipient is primordial.
No web application is an island anymore: with the explosion of the API economy, a typical application will have to integrate several external APIs. This is good news as it means one can focus on its core features while using existing services for all the ancillary tasks. The not-so-good news is that APIs can be flimsy: the provider can be down, the interface may have been carelessly changed, etc… Therefore, an application has to account for potential failures of the remote API and the related need to retry calls to it.
Thus, instead of directly interacting with a remote API, an application can decide to interact with it via a messaging channel and rely on both the already mentioned guaranteed delivery pattern and the transactional client pattern. These two patterns combined ensure that the message representing the desired interaction with the remote API will be delivered and redelivered until the remote call succeeds.
Of course, limiting the number of retries and dealing with undeliverable messages, for example if a provider decided to break its API, is essential to avoid thrashing on a request that simply will never be able to go through. This is when the dead letter channel pattern comes into play, as the dump truck destination of all messages that could not be delivered and are in need of a, potentially manual, failure cause analysis.
Using a messaging backbone can be a powerful tool to decouple the sub-systems of an application. One advantage of such an approach is that it acts as a natural circuit breaker: if one component of the application is in pain, the rest of it can still function well. For example, if the statistics part of the application is down, the rest of the system will keep working. When the statistics system will be back on-line, it will catch-up at its own pace and without disturbing anything.
But not all interactions are one-way. What about request-reply interactions? Does it make sense to use a messaging system for them when an HTTP request could do? Yes, it does make sense because the handling of the response can be delegated to a closure (callback method) called by a different thread. Instead of blocking the requester until the response is ready, as it’s typically done with HTTP requests (keeping in mind that there are asynchronous HTTP clients out there), the response handling code gets executed only when the response arrives. The request-reply and return address messaging patterns are both at play in that case.
Mouth watered? So is everything rosy in the messaging world? As always, there are caveats to consider. Let’s list a few and try to provide potential remediation strategies at the same time:
- Running highly available message brokers in-house can be complex: this can be alleviated by using messaging services like Amazon SNS/SQS, IronMQ or CloudAMQP. Or going broker less with ØMQ.
- Tracing what’s happening in a message-oriented distributed architecture can be daunting: the correlation identifier and message history patterns are handy if this is a concern.
- Some message brokers don’t guarantee the delivery ordering or can deliver the same message more than once: to deal with these situations, the resequencer and idempotent receiver patterns are respective solutions to consider.
- Message sizes are typically limited to several kilobytes: if you need to carry heavy payloads, store them in an external storage and refer to them in messages (for example with a URI),
- The transient nature of messages can be scary (what if everything crashes) or can even hinder auditing or statistics gathering: in that case the message store pattern will save the day!
- It is typically impossible or non-trivial to alter in-flight messages once they’ve entered a messaging broker: if you need to perform such operations, it’s better to consider using a shared database for storing messages and polling consumers to retrieve them.
- Testing message-oriented systems can be difficult: mine this white paper for ideas on how to achieve this.
So if you’re a cool kid coding your guts out in a hot startup or maybe a less-cool-less-kid developer dealing with architectural challenges, don’t be put off by the enterprisey sound of EIPs: take a look at them, it’s a treasure trove of architectural goodness!