A complete guide to messaging patterns with Vert.x and Java

When you build a micro services application, the question of inter-part communication arises. But good news are that you don’t need develop it from scratch – there is a concept called messaging, that we use to create communication between parts of distributed applications. In this post we would observe what is a message channel and which kinds of message channels are defined. And of course, we would see, how to code them using Vert.x and Java.

EventBus != Message Channel

The message channel is a device of communication, where the one side (Sender) writes information and the other side (Consumer) reads that information. In other words, the particular message channel is used to transmit data between concrete parts of the system (application).

Figure 1. Messaging channel

Here comes a temptation to call Vert.x’s EventBus a message channel, because it fits this definition. However, the role of EventBus is much broader, and in reality, it is an implementation of the idea of message channel, and as you would see in this post, we may create various types of message channels with EventBus, but its usage is not limited to this. In other words, assume that EventBus != Message Channel.

Types of enterprise messaging channel patterns

There are several kinds of messaging channels, and some are implemented by Vert.x natively (point-to-point, broadcasting), while other patterns we need to implement ourselves. Let review them one-by-one.

Point-to-Point channel

In point-to-point messaging, the data (message) is sent to the specific destination. In other words, this channel ensures that only one consumer receives the message. In case, we have multiple consumers with same address, only one can receive the message. Check a graph below, that represents this pattern:

Figure 2. Point-to-point messaging

If we move into practice, so how Vert.x assures that only one consumer receives the message? Well, if there is more than one handler is registered for the address, one will be chosen using a non-strict round-robin algorithm. It is easy to create this pattern, as it has a native implementation:

//sender
eventBus.send("address", data);

//receiver
eventBus.consumer("address", message->{
	/* Do something with message */
});

As you could note, it is simple and elegant. Also, there is a couple of things, I think, they are worth to mention. First, point-to-point messaging can have an optional answer from consumer back to sender. In Vert.x we can utilize a reply() method to do it (or also fail method if we want to indicate something bad):

//sender
eventBus.send("address", data);

//receiver
eventBus.consumer("address", message->{
	/* Do something with message */
	if (ok){
		message.reply(data);
	} else {
		message.fail(404, "No data found");
	}
});

The second thing, is about performance: note, that when you register a handler on a clustered event bus, it may take some time for the registration to reach all nodes of the cluster.

Publish – subscribe channel

Publish-subscribe channel is also called a broadcast channel, because the sender broadcasts the message to an each of multiple receivers. Take a look on a diagram – compare to point-to-point messaging, when the channel should assure, that there is only one successful consumer, in publish-subscribe channel, we deliver the message to all receivers.

Figure 3. Publish-Subscribe messaging

In Vert.x, broadcasting is even more elegant – that what you need to type:

//publisher
eventBus.publish("address", data);

Then in receivers we use same pattern, we observed early. So, the message will then be delivered to all handlers registered against the specified address.

Invalid message channel

Take a look on the graph first:

Figure 4. Invalid messaging channel

What does this type of channel in a nutshell? When we send a message and it is improper to the consumer, we move it to a specific channel, that is called Invalid message channel. It would contain messages that could not be processed by their receivers.

As this method is about a bad consuming, so we should implement it on the consumer’s side:

//sender code
//..

//consumer
eventBus.consumer("address", message->{
	//1.assert that message is improper
	//2.Move it to specific channel
});

How we can check is the message improper for the consumer? Well, it depends on your domain. There are different situations, most common are:

Bad data format

If we received a data, that we can deserialise into POJO, because of a wrong format, we would get a parsing exception, that we could catch and move message into an invalid channel:

eventBus.consumer("address", message->{
	try{
		JsonObject request = JsonObject.mapFrom(message.body());
	} catch (IllegalArgumentException e){

	}
});

Vertx JsonObject throws an IllegalArgumentException if conversion fails due to an incompatible type. We can catch this exception in order to assert a bad format data

Missed values

If a data is correct in format, but lacks of specific values, we can assert it in the aforesaid manner:

eventBus.consumer("address", message->{
	try{
		JsonObject request = JsonObject.mapFrom(message.body());
		if (!request.containsKey("key")){
			throw new NoSuchFieldException();
		}
	} catch(NoSuchFieldException e){

	}
});

Here we first validate, does data have a required key, and if no we throw new NoSuchFieldException.

Ok, we completed the first step – we assert that message is invalid, now we need to move it to the invalid message channel. Let start with creating a separate consumer invalid-channel:

//...

eventBus.consumer("address", message->{
	try{
		JsonObject request = JsonObject.mapFrom(message.body());
	} catch (IllegalArgumentException e){
		eventBus.send("invalid-channel", message.body());
	}
});

//..
private List<Object> invalidMessages;

eventBus.consumer("invalid-channel", message->{
	invalidMessages.add(message.body());
});

When a data is invalid, consumer sends it to the invalid channel. Then, we created a List of “invalid” objects, where we save invalid data.

Dead Letter channel

Again, here is a diagram (I love to draw them):

Figure 5. This is how Dead letter channel works

The difference between Dead letter channel and Invalid message channel is that in later case, the consumer decides that data is invalid and move it to a separate channel, and in the Dead letter pattern, a messaging system determines that if it cannot or should not deliver a message, it may elect to move the message. In other words, consumer assert fail, but sender moves a message. Check this code snippet:

//sender
eventBus.send("address", data, result->{
	//2. Sender receives an error
	if (res.failed()){
		//3. Sender MOVES a message
		eventBus.send("dead-letter", data);
	}
});

//receiver
eventBus.consumer("address", message->{
	try{
		JsonObject request = JsonObject.mapFrom(message.body());
	} catch (IllegalArgumentException e){
		//1. Message is bad -> tell a sender
		message.fail(404, "This is not for me");
	}
});

//create a dead-letter same way

As you noted, this pattern is implemented differently:

  1. Consumer assets that message is invalid, but inform a sender about it
  2. Sender checks if result of communication was failed
  3. Sender moves a message to a dead-letter channel
    In other things it is same – we can design a dead-letter channel in a similar manner as we did with the invalid message channel in the previous example.

Conclusion

As you can see, there are different types of messaging channels, so when you’re building a distributed architecture, you don’t need to reinvent a wheel. Moreover, two most common types – point-to-point and publish-subscribe – are implemented in Vert.x framework natively, while others can be defined easily, using existing components.

References

  • Enterprise Integration Patterns. Read