Designing beautiful AMQP APIs

Gabriel Francisco · October 22, 2017

As we all know microservices have become a huge trend that can’t be ignored. With their help, we can effectively decouple our systems, making them easier to test and manage. Beyond the well-known HTTP protocol, there are several other protocols and content types can be utilized to create APIs. Today, I’d like to share an experience about crafting APIs using AMQP, specifically with RabbitMQ (or any other message broker supporting AMQP 0.9.1).

Firstly, let’s clarify some essential AMQP concepts. However, if you’re already comfortable with AMQP, you might want to skip the next main topic and jump straight to designing APIs.

Basic AMQP Concepts

Nomenclature

In AMQP, Virtual Hosts, Exchanges, Queues, and Routing Keys should be written in lowercase, with words separated by dots. For instance:

colors
blue
dark.blue
green.dark.blue

Virtual Hosts

Virtual Hosts serve to isolate resources. A specific resource, such as exchanges or queues, can be used by more than one virtual host.

Routing Keys

Routing keys are strings provided by the producer and used by exchanges to redirect messages to queues. Note that not all exchanges use routing keys for message routing.

Exchanges

Exchanges act as the postmen of the system; they receive and redirect messages to queues. An exchange doesn’t hold messages; it only disseminates them.

Types of Exchanges

Direct: Direct exchanges route received messages to a queue based on its routing key. The routing key sent must match the binding routing key.

Fanout: Fanout exchanges indiscriminately redirect all messages to all bound queues.

Topic: Topic exchanges operate similarly to direct exchanges, but you can apply simple patterns to match a binding. ‘*’ substitutes exactly one word, and ‘#’ substitutes zero or more words.

Header: Header exchanges function like direct exchanges, but instead of using a routing key to match bindings, they use the headers from the sent message.

Queues

A queue stores and sends messages to consumers. You should always send a message to an exchange, which then redirects to the appropriate queue(s).

Bindings

You can bind either exchanges to queues or exchanges to other exchanges. If the exchange is not fanout, the binding will use routing keys or headers; otherwise, there’s no need for routing information.

Consumers

Consumers listen to queues and receive messages from them.

Acknowledgement

Acknowledgement can be set to either automatic or manual mode. In automatic mode, as soon as a consumer receives a message, the broker marks it as consumed. In manual mode, it waits until you acknowledge the message.

Designing APIs

With a solid understanding of the above concepts, we can now advance into designing APIs with AMQP.

Scenario

To give a practical example, let’s consider a simple scenario: credit card transactions. We will create three services: create, cancel, and fetch transactions. Additionally, we’ll need to track the events from services that create side-effects, such as the create and cancel services.

Exchanges as Domain Models

For this simple scenario, our only domain model will be the transaction domain. Thus, that’s the exchange we will create. For simplicity, let’s use a direct exchange, which is well suited for this situation.

Queues as Actions

The actions we need to perform are: create, cancel, or fetch a transaction. Hence, we can conceive of each of these actions as a different queue:

create.transaction
cancel.transaction
fetch.transaction

Additionally, since we need to save events, we’ll add one more queue:

save.transaction.side.effect.event

But how will fetch.transaction work since message sending is asynchronous?

You’re likely to send a message with a ‘X-Reply-To’ header, providing a routing key for the ‘fetch.transaction’ queue consumer to reply whether there’s a transaction for you to consume. If you’d prefer not to, there’s no issue with creating HTTP services to fetch resources instead of using AMQP services.

Routing Keys as Operations

Our operations - create, cancel, and fetch - will each be a routing key.

Bindings

This is where everything comes together. Depending on the routing key used (create, cancel, or fetch), we direct the message to the respective queue(s).

Here’s a clean representation of the bindings:

Exchange Routing Key Queues
transaction create create.transaction, save.transaction.side.effect.event
transaction cancel cancel.transaction, save.transaction.side.effect.event
transaction fetch fetch.transaction

So, do we forget about HTTP?

Not at all. HTTP is simpler and more widely understood than AMQP. Furthermore, “fetch” endpoints can be challenging to create with AMQP due to its asynchronous nature, requiring the creation of more “callback” consumers on the fly. As such, HTTP is often preferred. Remember, there’s no silver bullet in tech. Both AMQP and HTTP can work together, and it’s up to you to decide where to apply each.

Wrapping Up

I hope this post proves insightful. Learning how to design AMQP APIs wasn’t a walk in the park for me, and I hope my experience can make your journey a little bit easier.

Happy coding!

Twitter, Facebook