Edge Service using Quarkus and Mutiny
In this blog post we will see how, in a micro services architecture, we can leverage on Mutiny the Quarkus reactive framework to create an Edge Service and compose asynchronous HTTP API calls from other microservices.
The microservice architecture brings us to split our application into small, independent and loosely coupled services. Each service implementing a dedicated business capability within a dedicated bounded-context.
But, when you are developing a REST API that serves mobile or frontend applications this segmentation has a strong impact on how you will retrieve the data from your API.
Let’s take the following example :
I have a first microservices "Vehicle Service" responsible of the vehicle management (CRUD and other features around the vehicle). This microservice serves a HTTP API to retrieve the list of all the vehicles of a dedicated company.
In the vehicle informations we have the id of the driver assigned to this vehicle.
GET https://api.vehicles.acme.com/companies/56456416949616464/vehicles
[
{
"licencePlate": "220-AG-CX",
"brand": "Fiat",
"model": "E-Ducato",
"driverId": 1
},
{
"licencePlate": "856-PD-ZE",
"brand": "Peugeot",
"model": "e-Expert",
"driverId": 2
},
{
"licencePlate": "896-ZE-PL",
"brand": "Peugeot",
"model": "e-308",
"driverId": 3
},
{
"licencePlate": "999-EF-JK",
"brand": "Ford",
"model": "Mustang Mach-E",
"driverId": 4
},
{
"licencePlate": "111-EA-PQ",
"brand": "Fiat",
"model": "E-Ducato",
"driverId": 10
}
]
Then I have a second microservice, «Driver Service» responsible for the driver management. It serves an HTTP API allowing to retrieve a driver by its ID.
GET https://api.drivers.acme.com/companies/56456416949616464/drivers
[
{
"id": 1,
"firstname": "John",
"lastname": "Pittman"
},
{
"id": 2,
"firstname": "James",
"lastname": "Mercado"
},
{
"id": 3,
"firstname": "Daniela",
"lastname": "Pestine"
},
{
"id": 4,
"firstname": "Guillermo",
"lastname": "Clinico"
},
{
"id": 5,
"firstname": "Oliver",
"lastname": "Juz"
}
]
Now, let's say that I want to retrieve in my frontend application, all the vehicles, and their associated driver information. I’ll need to first retrieve the list of vehicles, and for each vehicle make an HTTP request to the driver service, to retrieve the driver information. Not really suitable and performant…
While from my old monolith I was able to retrieve all the information in a single request (SQL join FTW), I’m now moving the complexity to collect and merge the data on the frontend side.
With an impact in terms of cost if your frontend already exists as you’ll need to refactor the logic to retrieve the data. And potentially performance impact. More HTTP requests to perform, more network and latency induced…
That’s where the notion of Edge Service (or API Gateway Service) comes to the rescue. The Edge Service is a dedicated microservice with the responsibility to invoke the services that own the data and perform in-memory joins of the results before serving it to a client.
In our example, the edge service will have the responsibility to expose an HTTP REST API to get the company vehicles enriched with the driver information
Behind the scene it will :
- Retrieve the list of the company vehicles from the vehicle Microservices
API - Aggregate for each vehicle the associated driver from the driver Microservices API
Let's start to create a new Quarkus project for our new edge service with the following maven command :
mvn io.quarkus.platform:quarkus-maven-plugin:2.7.1.Final:create \
-DprojectGroupId=com.michelin \
-DprojectArtifactId=edge-service \
-Dextensions="quarkus-resteasy-reactive, quarkus-resteasy-reactive- jsonb, smallrye-mutiny-vertx-web-client" \
-DnoCode
cd edge-service
It creates a project importing RESTEasy/JAX-RS + JSONB reactive extensions and a Vert.x Web client with Mutiny binding.
It will add the following dependencies to your pom.xml file :
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jsonb</artifactId>
</dependency>
Reactive approach
As this new microservice will be the front door for our frontend applications, it will need to handle multiple concurrent requests.
With Quarkus you can easily switch from an imperative programming model to a reactive one.
All the details about the difference between the two approaches and the advantages of the reactive one are well explained here : https://quarkus.io/guides/quarkus-reactive-architecture
The main reason for us to choose a reactive approach on this component will be the scalability and the ability to handle a large amount of concurrent requests.
To do this, Quarkus comes with Mutiny - an intuitive and event-driven reactive programming library.
With Mutiny everything is event-driven: you receive events, and you react to them. This event-driven aspect embraces the asynchronous nature of distributed systems and provides an elegant and precise way to express continuation.
Creating Rest Clients
The first thing to do is to create two HTTP Rest Client to consume both the Vehicle service API at `api.vehicle.acme.com` and the Driver service API at `api.driver.acme.com`
Then we create two REST Client Service
DriverService
We first setup a POJO class to map the data returned by the Driver API.
package org.acme.rest.json;
public class Driver {
public int id;
public String firstname;
public String lastname;
}
Then we create the Driver Service Rest Client
package org.acme.rest.client;
import io.smallrye.mutiny.Uni;
import org.acme.rest.json.Driver;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@RegisterRestClient(configKey = "driver-api")
public interface DriverService {
@GET
@Path("/drivers/{id}")
Uni<Driver> getDriversById(@PathParam("id") int driverId);
}
The return type we are using for our getDriverById
method is a Mutiny Uni
which is convenient to represent asynchronous actions that return 0 or 1 result
VehicleService
We do the same for the vehicle service
package org.acme.rest.json;
public class Vehicle {
public String licencePlate;
public String brand;
public String model;
public int driverId;
}
package org.acme.rest.client;
import io.smallrye.mutiny.Uni;
import org.acme.rest.json.Vehicle;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import java.util.List;
@RegisterRestClient(configKey = "vehicle-api")
public interface VehicleService {
@GET
@Path("/companies/{id}/vehicles")
Uni<List<Vehicle>> getVehicles(@PathParam("id") int companyId);
}
Configuration
We now need to configure our two Rest Service to reach our two backend microservices.
We use the @RegisterRestClientconfigKey
annotation on our class. This annotation allow us to define with the following properties the base url used by the Rest client.
# application.properties
quarkus.rest-client.vehicle-api.url=https://api.vehicles.acme.com
quarkus.rest-client.driver-api.url=https://api.drivers.acme.com
Time to aggregate !
Let's now create a new service class called VehicleDriversService
that will be responsible to aggregate the vehicles with their associated drivers, using our two RestClient. This aggregate will use the following VehicleDriver POJO
package org.acme.rest.json;
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class VehicleDriver {
public Vehicle vehicle;
public Driver driver;
public VehicleDriver(Vehicle vehicle, Driver driver) {
this.vehicle = vehicle;
this.driver = driver;
}
}
package org.acme.rest.service;
@ApplicationScoped
public class VehicleDriversService {
@RestClient
DriverService driverService;
@RestClient
VehicleService vehicleService;
public Uni<List<VehicleDriver>> getVehicleWithDrivers(int companyId) {
return vehicleService.getVehicles(companyId)
.onItem()
.transformToMulti(vehicles -> Multi.createFrom().iterable(vehicles))
.onItem().transformToUniAndMerge(this::getDriverByVehicle)
.collect().asList();
}
private Uni<VehicleDriver> getDriverByVehicle(Vehicle vehicle) {
return this.driverService.getDriversById(vehicle.driverId)
.map(driver -> new VehicleDriver(vehicle, driver));
}
}
-
First, we inject the
driverService
andvehicleService
RestClient using the @RestClient annotation -
Then we create a
getVehicleWithDrivers
method. For a given company ID it will return a List of VehicleDriver POJO that wraps the vehicle and driver models -
vehicleService.getVehicles(companyId)
: We retrieve aUni<List<Vehicle>>
-
Once we get this List we transform it to a stream (Multi) using
onItem().transformToMulti()
. -
Then, in parallel for each vehicle of the upstream, we retrieve the associated driver using
onItem().transformToUniAndMerge()
. For people familiar with RXJava it correspond to the flatMapSingle operation. -
Finally, we collect the result as a List and obtain a
Uni<List<VehicleDriver>>
It's now time for our Edge Service to expose a dedicated HTTP route that will use this `VehicleDriversService` to expose the aggregated vehicles and drivers for a given company
@Path("/companies")
public class VehicleDriverResource {
@Inject
VehicleDriversService vehicleDriversService;
@GET
@Path("/{id}/vehicles")
public Uni<List<VehicleDriver>> getByCompany(@PathParam("id") int id) {
return vehicleDriversService.getVehicleWithDrivers(id);
}
}
GET https://api.edge-service.acme.com/companies/56456416949616464/vehicles
[
{
"vehicle": {
"licencePlate": "220-AG-CX",
"brand": "Fiat",
"model": "E-Ducato",
"driverId": 1
},
"driver": {
"id": 1,
"firstname": "John",
"lastname": "Pittman"
}
},
{
"vehicle": {
"licencePlate": "856-PD-ZE",
"brand": "Peugeot",
"model": "e-Expert",
"driverId": 2
},
"driver": {
"id": 2,
"firstname": "James",
"lastname": "Mercado"
}
},
{
"vehicle": {
"licencePlate": "896-ZE-PL",
"brand": "Peugeot",
"model": "e-308",
"driverId": 3
},
"driver": {
"id": 3,
"firstname": "Daniela",
"lastname": "Pestine"
}
},
{
"vehicle": {
"licencePlate": "999-EF-JK",
"brand": "Ford",
"model": "Mustang Mach-E",
"driverId": 4
},
"driver": {
"id": 4,
"firstname": "Guillermo",
"lastname": "Clinico"
}
},
{
"vehicle": {
"licencePlate": "111-EA-PQ",
"brand": "Fiat",
"model": "E-Ducato",
"driverId": 10
}
}
]
TADAM! calling this route gives us an array of vehicles and associated drivers aggregated. If no driver is found for a vehicle, the driver section is just omitted.
In this short example, we have seen how Quarkus and especially Mutiny could help us to compose different asynchronous operations.
Obviously, in terms of performance, the reactive approach we chose on the edge service will be dependent of the other services response time. And as often the bottleneck is not the HTTP side, but the I/O on the data layer. But it's another subject for another blog post ;).