Basic web app architecture for Vert.x 4.x

Unlike Spring, Vertx does not have strong architectural paradigms; this fact is not bad, because developers are not forced to follow prescribed rules and have true freedom to implement own patterns. From the other side, it confuses programmers and they begin to reinvent a bicycle each and every time. This is a reason why I defined several architectural concepts that work great with Vertx and I would like to share them with you. They were not invented yesterday: I keep using them since early 2019, when I published my first Vert.x post and I improve them based on my practical experience and for sure based on changes, that are introduced with new releases.

Ok, let start. As Java developers we know two main approaches to organize web applications: by technical layer and by business domain. With monolith apps there are certain clashes and fights between various schools, like “packaging by layer died!” or “packaging by layer is better than by domain” etc. However, when it comes to microservices we do not need to choose what is right and what is wrong. From the technical point of view, microservices implement both paradigms:

  • Horizontally: each service is scaled down to a specific business domain
  • Vertically: technical components inside the service are organized based on their layer (data, web, etc)

When it comes to practice, we can stick with these concepts for our Vertx applications. I talk about various architectural patterns for Vertx in more details in my book “Principles of Vertx“, but in a nutshell, all of them use verticles as building blocks. In this post we will review and implement a basic architecture, that includes three verticles, encapsulated by the technical level: DataVerticle, RouterVerticle and AppVerticle. Verticles communicate using Eventbus messaging patterns (usually, request-response). In next subsections we will understand a purpose of each of them.

Server and router

The main foundation of web applications with Vertx is the Router component. Unlike the server class, that comes with the core library, the router requires the web dependency. In a nutshell, the router in Vertx implements same principles as the Express.js router. The router servers as an entry point that allows to attach request handlers (similar to Express.js middleware functions). The router listens for incoming requests, and when it arrives, calls the first matching handler to process it. The handler can terminate the request flow and return the response or it can pass the request to the next matching handler. The later concept allows to build handler pipelines.

In order to use router we need to create a new instance and then to attach this instance to the HttpServer in order to listen for incoming requests.

@Override
public void start(Promise<Void> startPromise){
    HttpServer server = vertx.createHttpServer();

    Router router = Router.router(vertx);

    server.requestHandler(router);

    Future<HttpServer> result = server.listen(8080);
    result.onSuccess(r -> startPromise.complete())
            .onFailure(e -> startPromise.fail(e));
}

In this example we allocate the web logic inside the start method of the RouterVerticle. Note, that we use Vertx 4.x Futures to handle result of the server deployment. We hardcoded the HTTP port (8080) for simplicity, but you need to avoid such practice by adding a dependency injection and to provide a port value based on an application configuration. We can also use ServerOptions to define the server port, as well other parameters as following:

HttpServerOptions serverOptions = new HttpServerOptions();
serverOptions.setPort(8080);
HttpServer server = vertx.createHttpServer(serverOptions);

//.. router initialization

Future<HttpServer> result = server.listen();
result.onSuccess(r -> startPromise.complete())
        .onFailure(e -> startPromise.fail(e));

The router class allows to attach handlers – functions that do request/response processing. For that we first need to create a route (which defines a method and a path combination to match) and then we attach handlers to it:

router.get("/hello").handler(context -> context.response().end("Hello, World!"));

From a technical point of view, a handler is essentially a lambda function, that supples a reference to the context data. The context stays for the duration of the request lifecycle and is passed to next handlers, if the request is not terminated (ended). We can decompose handler to separate methods and connect them using method references. Take a look on the following example implementation:

router.delete("/project/:id").handler(this::deleteProject);

//..
void deleteProject(RoutingContext context){
        JsonObject payload = new JsonObject();
        payload.put("id", context.pathParam("id"));
        Future<Message<JsonObject>> message = vertx.eventBus().request("projects.db.remove", payload);
        message.compose(r -> context.response().setStatusCode(201).end(r.body().encodePrettily()));
}

If you need to access the request body payload inside handler functions, you need to set up a body handler before routes, that will use the body payload. I recommend you to attach a body handler to the route mask * (this is a wildcard – that means it will be available for all routes):

router.route("/*").handler(BodyHandler.create());

Connection to the DataVerticle

Let take a more real life example. We work on an application, that exposes an API to manage projects and we want to implement it in Vertx 4.x. An undoubt strength of the framework is its flexibility – developers do not need to follow a number of predefined paradigms and patterns. From the other side, it is also a weak place, because engineers become confused and try to reinvent a bycicle every single time. In a nutshell, all of them use verticles as a foundation block of its structure.

When it comes to monolith applications, there are controversial opinions on structuring apps (by technical layer or by business domain). With microservices a situation is different: services are decoupled based on their domain and inside they are organized based on the technical layer. This is a basic architectural pattern that I use for Vertx applications. We decompose code into three core verticles:

  • Router (web) verticle – this component exposes HTTP API, does server side validation, performs authentication checks (token assertion)
  • Data verticle – this component is responsible for underlaying database operations (such as saving or retrieving data)
  • App verticle – this component is used in order to initialize and deploy previous two verticles

In this section we will observe how does the data verticle work. Note: I recommend to deploy this verticle as a worker verticle, because database operations are considered consuming and blocking, even if you use Vertx-provided database clients. This will keep the data verticle outside of the main thread. The data verticle and the web verticle are connected with the EventBus request-response messaging.

Basically, the data verticle exposes a number of messaging consumers, that handle requests from the API verticle and do some database operations. My personal preference is to abstract database-specific logic from the verticle by using a repository pattern or a data access object pattern. We will not cover concrete implementations in this post – if you are curious, I use here the embedded Nitrite NoSQL data storage.

Take a look on the following code snippet, which demonstrates the sample data verticle:

@Override
public void start(Promise<Void> startPromise) throws Exception {
vertx.eventBus().consumer("projects.db.create", r -> {
        JsonObject body = JsonObject.mapFrom(r.body());
        JsonObject result = dao.createProject(body);
        r.reply(result);
});
vertx.eventBus().consumer("projects.db.all", r -> {
        JsonArray projects = dao.findAll();
        JsonObject result = new JsonObject().put("projects", projects);
        r.reply(result);
});
vertx.eventBus().consumer("projects.db.one", r -> {
        JsonObject payload = JsonObject.mapFrom(r.body());
        JsonObject result = dao.findOneProject(payload.getString("id"));
        r.reply(result);
});
vertx.eventBus().consumer("projects.db.remove", r -> {
        JsonObject payload = JsonObject.mapFrom(r.body());
        String id = payload.getString("id");
        dao.remove(id);
        r.reply(new JsonObject().put("status", true));
});
startPromise.complete();
}

Please note, that due to the fact, that the data verticle is deployed as a worker verticle (e.g outside of the main thread), we don’t explicitly handle data access operations in a reactive way, so possibly (while it is not the best way, yet acceptable) you can use “classic” synchronous APIs with Vertx in this way.

Back to the web verticle

On the other end of the pipeline, we have handlers in the web verticle component. In general, handlers are decomposed from the start() function using method references. Because they implement request messaging, we provide a HTTP response handling, once the Message Future is completed:

void createProject (RoutingContext context){
        JsonObject body = context.getBodyAsJson();
        Future<Message<JsonObject>> message = vertx.eventBus().request("projects.db.create", body);
        message.compose(r -> context.response().setStatusCode(201).end(r.body().encodePrettily()));
}

void getAllProjects (RoutingContext context){
        Future<Message<JsonObject>> message = vertx.eventBus().request("projects.db.all", new JsonObject());
        message.compose(r -> context.response().setStatusCode(201).end(r.body().encodePrettily()));
}

void getProject(RoutingContext context){
        JsonObject payload = new JsonObject();
        payload.put("id", context.pathParam("id"));
        Future<Message<JsonObject>> message = vertx.eventBus().request("projects.db.one", payload);
        message.compose(r -> context.response().setStatusCode(201).end(r.body().encodePrettily()));
}

void deleteProject(RoutingContext context){
        JsonObject payload = new JsonObject();
        payload.put("id", context.pathParam("id"));
        Future<Message<JsonObject>> message = vertx.eventBus().request("projects.db.remove", payload);
        message.compose(r -> context.response().setStatusCode(201).end(r.body().encodePrettily()));
}

We can also specify content types for both requests and responses using respectively consumes and produces methods as it is shown below:

router.post("/projects").consumes("application/json").produces("application/json").handler(this::createProject);
router.get("/projects").consumes("application/json").produces("application/json").handler(this::getAllProjects);
router.get("/project/:id").consumes("application/json").produces("application/json").handler(this::getProject);
router.delete("/project/:id").consumes("application/json").produces("application/json").handler(this::deleteProject);

Source code

This post is a part of a big series of publications in my blog about Vert.x 4.x. You can find a source code for all articiles in this github repository. Feel free to explore it.