Building services and APIs with AMQP 1.0

Mi­croser­vices and APIs are every­where. Every­one talks about them, pre­sen­ta­tion slides are full of them … some peo­ple are ac­tu­ally even build­ing them. Mi­croser­vices and APIs are of course not com­pletely new con­cepts and they are a bit over-​hyped. But in gen­eral the ideas be­hind them are not bad. Un­for­tu­nately, many peo­ple seem to be­lieve that the only way how to im­ple­ment an API in mi­croser­vice is to use HTTP and REST. That is of course not true. Mi­croser­vices and APIs can be based on many dif­fer­ent pro­to­cols and tech­nolo­gies. My fa­vorite one is of course AMQP. Don’t take me wrong, HTTP and REST is not nec­es­sar­ily bad. But in some cases AMQP is sim­ply bet­ter and cre­at­ing AMQP based APIs does not need to be com­pli­cated.

This is a re-​publication of the fol­low­ing blog post

LiveScore service

For demon­stra­tion, I will use a very sim­ple ser­vice for keep­ing scores of foot­ball games. It has very basic API. It has only three calls:

  • Add a new game
  • Up­date a score of ex­ist­ing game
  • List the scores The AMQP vari­ants will be ad­di­tion­ally able to push live up­dates to the clients.

The demo is using Java and Vert.x toolkit. Vert.x is cool and I def­i­nitely rec­om­mend it to every­one. But most of the stuff from the demo should be pos­si­ble also in any other pro­gram­ming lan­guages and/or frame­work.

HTTP API

HTTP im­ple­men­ta­tion of my ser­vice is a typ­i­cal REST API. Since it is very sim­ple, it ac­cepts re­quests only on one end­point – /api/v1.0/scores. New games are added as POST op­er­a­tions, scores are up­dated with PUT op­er­a­tions and list of all scores can be ob­tained with GET.

With Vert.x, cre­at­ing HTTP/REST API is very easy. First the web router has to be cre­ated with all planned API calls:

router = Router.router(vertx);  
router.route("/api/v1.0/*").handler(BodyHandler.create());  
router.get("/api/v1.0/scores").handler(this::getScores);  
router.post("/api/v1.0/scores").handler(this::addGame);  
router.put("/api/v1.0/scores").handler(this::setScore);  

Then the HTTP server has to be cre­ated and linked with the router:

HttpServerOptions httpOptions = new HttpServerOptions();  
server = vertx.createHttpServer(httpOptions)  
   .requestHandler(router::accept)  
   .listen(httpPort);  

And fi­nally the han­dlers which will be trig­gered for each API call have to be im­ple­mented as well. The full code is on GitHub.

The HTTP API doesn’t pro­vide any way how to au­to­mat­i­cally push the score up­dates to the clients. The clients sim­ply have to poll the ser­vice pe­ri­od­i­cally to get the up­dates. HTTP has of course some ways how to push live up­dates to clients. For ex­am­ple, with Web­Sock­ets or with chun­ked trans­fers. How­ever, these are not that easy to im­ple­ment. The ser­vice would also need to keep sep­a­rate con­nec­tion with every client and push the up­dates for each of them sep­a­rately.

AMQP API

Cre­at­ing the HTTP API was re­ally easy. Cre­at­ing an AMQP API has to be more com­pli­cated, right? We would need an AMQP server, which will lis­ten on some port, ac­cept the con­nec­tions, ses­sions, links and so on. There are usu­ally no nice and sim­ple to use li­braries for this.

Sure, this is one way how to do it. There is ac­tu­ally a nice li­brary called Apache Qpid Pro­ton. It has Java and C ver­sions and bind­ings into many other lan­guages (Go, C++, Python, …). It makes cre­at­ing your own AMQP server lot eas­ier. It will take care of de­cod­ing and en­cod­ing the AMQP pro­to­col, han­dling the con­nec­tions, ses­sions etc. But still, Qpid Pro­ton is not even nearly as easy to use as the HTTP router used for the HTTP API.

API with AMQP server

Are there any eas­ier op­tions? What if all what is needed to cre­ate AMQP based API is a sim­ple AMQP client? Nor­mally, that should not be a pos­si­ble be­cause we need the API to lis­ten on some port for the clients to con­nect to it and send re­quests. And clients usu­ally don’t lis­ten on any ports. How­ever, Apache Qpid has some­thing called Dis­patch. It works as a light­weight AMQP router. Dis­patch will serve as the AMQP server which was miss­ing. It will take care of han­dling client con­nec­tions, se­cu­rity and shield the ser­vice from the ac­tual clients. All the ser­vice needs to do is to use AMQP client to con­nect to Dis­patch on pre­de­fined ad­dress and wait for the re­quest.

AMQP API with Dispatch router

Dis­patch needs to be con­fig­ured with three API entry points as ad­dresses:

address {  
    prefix: /setScore  
    distribution: balanced  
}  
address {  
    prefix: /getScore  
    distribution: balanced  
}  
address {  
    prefix: /addGame  
    distribution: balanced  
}  

LiveScore ser­vice will con­nect to these ad­dresses as a re­ceiver / con­sumer. Clients will con­nect to them as senders /pro­duc­ers. And Dis­patch will take care of rout­ing the mes­sages be­tween the clients and the ser­vice. Clients can also cre­ate ad­di­tional re­ceivers so that the ser­vice is able to re­spond to their re­quests and spec­ify the ad­dress of the re­ceiver as the reply-​to header in the re­quest mes­sage. LiveScore ser­vice will au­to­mat­i­cally send the re­sponse to this ad­dress. But spec­i­fy­ing a reply-​to is not manda­tory. If the client wants, it can sim­ply fire the re­quest and for­get about the re­sponse.

LiveScore ser­vice is using Vert.x AMQP Bridge which al­lows easy in­te­gra­tion be­tween the Vert.x Event Bus and the AMQP con­nec­tion to my router. The ser­vice starts the AMQP Bridge and if it suc­cess­fully con­nects to Dis­patch it cre­ates three re­ceivers for the API calls.

AmqpBridgeOptions options = new AmqpBridgeOptions().addEnabledSaslMechanism("ANONYMOUS");  
bridge = AmqpBridge.create(vertx, options);  
bridge.start(amqpHostname, amqpPort, res -> {  
   if (res.succeeded())  
   {  
     bridge.createConsumer("/setScore").setMaxBufferedMessages(100).handler(this::setScore);  
     bridge.createConsumer("/getScores").setMaxBufferedMessages(100).handler(this::getScores);  
     bridge.createConsumer("/addGame").setMaxBufferedMessages(100).handler(this::addGame);  
     fut.complete();  
   }  
   else  
   {  
     fut.fail(res.cause());  
   }  
});  

The only other thing which needs to be done is cre­at­ing han­dlers for han­dling the re­quests re­ceived from clients:

public void getScores(Message<Object> msg)  
{  
   if(msg.replyAddress() != null)  
   {  
     JsonObject response = new JsonObject();  
     response.put("application_properties", new JsonObject().put("status", 200));  
     response.put("body", new JsonArray(Json.encode(scoreService.getScores())).encode());  
     msg.reply(response);  
   }  
   else  
   {  
     LOG.warn("Received LiveScore/getScores request without reply to address");  
   }  
}  

Live broad­cast­ing of score up­dates is also very easy. New ad­dress has to be added into Dis­patch con­fig­u­ra­tion. This ad­dress will be used in op­po­site di­rec­tion. the ser­vice con­nects to it as sender / pro­ducer and clients which want to re­ceive the live up­dates cre­ate a re­ceiver against this ad­dress. What is im­por­tant, this ad­dress has to be marked as mul­ti­cast. Thanks to that every sin­gle mes­sage will be de­liv­ered to all con­nected clients and not just to one of them:

address {  
    prefix: /liveScores  
    distribution: multicast  
}  

Multicasting messages

Thanks to the mul­ti­cast dis­tri­b­u­tion, the ser­vice doesn’t need to send a sep­a­rate up­date to every sin­gle client. It sends the mes­sage only once and dis­patch takes care of the rest.

public void broadcastUpdates(Game game)  
{  
   LOG.info("Broadcasting game update " + game);  
   JsonObject message = new JsonObject();  
   message.put("body", new JsonObject(Json.encode(game)).encode());  
   producer.send(message);  
} 

Again, the com­plete source codes of the demo ser­vice are avail­able on GitHub.

How to structure AMQP APIs?

Com­pared to HTTP and REST, AMQP gives its users a lot more free­dom when de­sign­ing the API. It isn’t tied up by the avail­able HTTP meth­ods.

My LiveScore ser­vice is using the API end­points named ac­cord­ing to their func­tion:

  • /LiveScore/ad­dGame
  • /LiveScore/setScore
  • /LiveScore/getScores It also uses HTTP sta­tus codes in ap­pli­ca­tion prop­er­ties of the dif­fer­ent mes­sages to de­scribe the re­sult of the re­quest and JSON as the mes­sage pay­load with the ac­tual re­quest and re­sponse.

Is that the best way? To be hon­est, I don’t know. Just for the re­quest en­cod­ing there are many dif­fer­ent op­tions. AMQP has its own en­cod­ings which sup­ports all pos­si­ble basic as well as more ad­vanced data types and struc­tures. But AMQP can also trans­fer any opaque data - be it JSON, XML, Google Pro­to­col Buffers or any­thing else. For sim­ple re­quest, the pay­load can be com­pletely skipped and ap­pli­ca­tion prop­er­ties can be used in­stead. And for every­one who re­ally loves HTTP/REST, one can also model the API in REST style as I did in an al­ter­na­tive im­ple­men­ta­tion of my demo ser­vice.

Browser

One of the en­vi­ron­ments where HTTP is so to say “at home” is browser. AMQP will prob­a­bly never be as “na­tive” pro­to­col for any browser as HTTP is. How­ever AMQP can be used even from browsers. It has Web­Socket bind­ing and there are Javascript AMQP li­braries - for ex­am­ple rhea. So AMQP can be also used re­ally every­where.

Decoupling

It is im­por­tant to men­tion that the Dis­patch router doesn’t de­cou­ple the client from the ser­vice. If de­cou­pling is what is needed, it can be eas­ily achieved by re­plac­ing the Dis­patch router with some AMQP bro­ker. The bro­ker would de­cou­ple the client from the ser­vice with­out any changes in the ser­vice or clients.

Conclusion

While cre­at­ing APIs using AMQP can be very easy, it doesn’t mean that AMQP is the best pro­to­col for all APIs. There are def­i­nitely APIs where HTTP is more suit­able. But in some use cases, AMQP has clear ad­van­tages. In my LiveScore ex­am­ple it is es­pe­cially one to many com­mu­ni­ca­tion. It is im­por­tant to keep the mind open and se­lect the best avail­able for given ser­vice.

Next post

An Introduction to the Vert.x Context Object

Under the hood, the vert.x Context class plays a critical part in maintaining the thread-safety guarantees of verticles. Most of the time, vert.x coders don't need to make use of Context objects directly.

Read more
Previous post

Internet of Things - Reactive and Asynchronous with Vert.x

I have to admit … before joining Red Hat I didn’t know about the Eclipse Vert.x project but it took me few days to fall in love with it!

Read more
Related posts

Some Rest with Vert.x

This post is part of the Introduction to Vert.x series. Let’s go a bit further this time and develop a CRUD-ish application

Read more

Vert.x Web API Service Introduction

This blog post teaches you how to use the new module vertx-web-api-service to combine the Web Router and the OpenAPI Router Factory with service proxies.

Read more

Unit and Integration Tests

Let’s refresh our mind about what we developed so far in the introduction to vert.x series. We forgot an important task. We didn’t test the API.

Read more