BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations Techniques for Maintainable Quarkus Applications

Techniques for Maintainable Quarkus Applications

Bookmarks
37:28

Summary

Ana Maria Mihalceanu discusses how to use Quarkus capabilities in order to write software that is easier to maintain.

Bio

Ana Maria Mihalceanu is a Java Champion, Certified Architect, co-founder of Bucharest Software Craftsmanship Community, and an adopter of challenging technical scenarios involving Java-based frameworks and multiple cloud providers. She is an active supporter of technical communities’ growth through knowledge sharing and enjoys curating content for conferences as a program committee member.

About the conference

QCon Plus is a virtual conference for senior software engineers and architects that covers the trends, best practices, and solutions leveraged by the world's most innovative software organizations.

Transcript

Mihalceanu: Welcome to techniques for maintainable Quarkus applications. I'm Ana. I'm a Java champion, and working as an architect in my day-to-day life. I also like to share my experiences and experiments at different conferences or within the Bucharest Software Craftsmanship Community, which I co-founded together with my dear friend, Victor Rentea.

Software Maintainability

In today's talk, I will showcase a few techniques to write maintainable code or help you write maintainable code. The reason is that maintainable software can reduce not only a system's lifecycle cost but can also help enhance the way your productivity is going on in your day-to-day life as a developer. I'm speaking about here that maintainable software means mainly that you are easily repairing. Not only this, but you're also evolving what you have already written or what others have written straightforwardly. You do not have to rewrite everything. You are not afraid of writing code in a specific part of your application.

Microservices

Since we have just started the conversation about maintainability, one architecture that supports writing highly maintainable software systems and is very much loved is microservices architecture. The reason is that this type of distributed architecture allows you to work in an isolated mode at mini-applications that can be orchestrated to do what your end-users expect as functionalities from the system you're building. Probably some of you already have worked with microservices and experienced that you can develop in isolation, not wait for other applications to be ready to do development on your own, fix your application, and, of course, deploy it. This architecture allows you to do your work regardless of what's happening around you and is a very good stimulant for writing more maintainable code.

Why Quarkus?

As I spoke about microservices, you can choose different languages when you're writing those. When you're looking to write microservices with Java, you have several options. Quarkus is one of them. Quarkus is an open-source Kubernetes native Java framework tailored for GraalVM and OpenJDK Hotspot. A few things have drawn me as a developer, and I pretty much like the experience I have had so far with Quarkus. Besides the unified configuration, I like that I have live reload. Secondly, I liked Quarkus because when working with persistence units, like working with the database, I worked with JPA or Hibernate; now, I can also use Panache to help me interact with that database.

Last but not least, I'm also coming from the Spring developer world: I've worked with Spring MVC, I've worked with Spring Boot. Quarkus has Spring API compatibility. If you're used to Spring, and you want to go into learning Quarkus quickly, you can do it. You can combine both by using the Spring API compatibility. In this union, Quarkus brings you the benefit of early detection of dependency injection errors at compile time and not at runtime when you're using both of them.

Overview

Of course, we will write more maintainable code for an application. The application that we're going to stick together today is called StitchingFacts. The StitchingFacts is a Quarkus application, and we want to write more maintainable code and enhance a few things around StitchingFacts. StitchingFacts is exposing an API for consumption. The facts that are being stitched within it are consumed from a Cat facts API (https://cat-fact.herokuapp.com) maintained separately from my system. It's not under my control. I'm just consuming information that I need to manipulate more for my end-users to receive and read or for other applications to ingest. Sometimes, when I'm interacting with this cat facts API, I want to keep a few things from it. I've used the stitchy PostgreSQL database to keep the things that I'm interested in from the cat facts API, so that I don't go every time to the API and make some round trips that cost me in the duration of the requests.

Demo - Let's Code

Before going to the IDE, I will give you a brief overview that when working with Quarkus, you can start working with Quarkus and generate your Quarkus application at https://code.quarkus.io/. You can choose whatever extensions you like. My application has some code and uses some extensions, and we're going to enhance it. We're writing maintainable applications, and the expectation is that something is already written there.

Let's go to the IDE. The StitchingFacts app has a few details to bring to your attention. First of all, like any Quarkus app that you're going to generate with some boilerplate code, it has a generated endpoint. In my case, I have refactored it a bit. It's called FactsEndpoint. It's going to be the one that's going to expose the migrated API to the end-users or forward to other applications for consumption. It has application.properties, where the unified configuration is staying. I'm keeping some configs for my app, and of course, some tests.

The first place we're going to stop is the application.properties, where I established some global details for my app. What is going on here is that I have my Cat facts API available at this URL ( https://cat-fact.herokuapp.com). It's connected inside my application code, my Java code as a REST client, through the FactsService interface. I've combined this single line that I'm consuming from this URL via the FactsService interface, registered as a REST client. When consuming the service, I inject this interface into my code. Of course, there is some elegance in this approach because I don't have to define another variable for the URL. The mapping between the two is happening in the application.properties.

Looking a little bit down, I'm working in my local right now. I have connected a local host PostgreSQL database, which is running, of course, on 5432, my stitchy local database. If I'm keeping the current configuration, it means is that I'm always going to see a lot of logs. Whenever my application is going to run somewhere else, not only on local, I'm going to get a lot of logs. A lot of log information might not be valuable for everybody in a project. You're probably working in a team. Is annoying if you get too much info coming from one application, not to mention if you're working in a microservices environment that you're keeping an eye on many more. Also, you would get too much irrelevant information from your app from a DevOps point of view.

First of all, the Quarkus log level, I've set the global one to FINEST, which is a good idea if you want to get a lot of information. Typically, you're not interested in getting so much information. For example, this one on FINEST says, Query Executor Implementation, BE Parse Complete Null; this information might not be helpful. Furthermore, I'm interested in seeing how my app is behaving. I would advise you not to log on info or error unless something would benefit from being there in your log files. I would suggest using debug most of the time when you want to put some places that can give you information about what's going on with your app. If you're working on your local, you can also use Trace, but please be aware that a colleague that might use this configuration might be bothered when going further. If I want to have some differences in the profiles of my app, I can use the dev prefix for my local and tailor this configuration to be happening only for dev so that I'm not disturbing the production model of the app. Whenever somebody gets this on their local, they will experience the same configurations I shared earlier.

When you're looking to production, production has a similar view as dev, with the difference that I'm introducing some variables here because I don't know the username and the password for the production database, and those should be secret anyway. One thing that I did for my logs was to use some variables. It is much easier for you to control the log level from the pipeline, from the environment. You expect that information to come from the environment and not be hardcoded in your app. However, if you're worried that this might not be defined at the level of your pipeline and you don't want to break things, you can specify a default value.

Looking at the test profile part, this is pretty much similar to the other ones. When I'm testing, I wouldn't like to use a PostgreSQL database because that would introduce a dependency, like always we need to have with PostgreSQL database somewhere available. That is a bit resource-consuming and troubling for you to know credentials all the time. So I'm using an in-memory database. And I have created different SQL load-scripts. Because for my application, I had to define primary key generation differently since the in-memory database has another way of handling primary keys from the PostgreSQL database. I'm just using information that is very useful for testing purposes.

Let's move to the next step for our app. When you are writing endpoints for consumption in our application, one thing that you are thinking about should be asynchronous or synchronous when you're creating microservices? To help you decide and have a good decision around that, whether synchronous or asynchronous, you can make synchronous and asynchronous implementations for the same endpoint. They consume the same FactsService endpoint from the Cat facts API, and you are doing this just to assess how these two are going, which one is better and which one to keep. I'm going to compare their response times. My bold assumption is that synchronous time is three times bigger than asynchronous. Let's do some tests around that. First of all, I need to establish a few things, like creating something that would help me measure the time. I'm using a Micrometer Timer to measure the synchronous requests, and then I'll go with another Micrometer Timer for the asynchronous requests. I'm just changing a few things here: measure that when executing five requests to the synchronous endpoint. I will do the same thing for my asynchronous requests and measure those. Let's just run the unit test now.

While this is running in the background, and the expectation is that synchronous time will be bigger than the asynchronous one, we're going to move to the next part: looking at how you could improve what's going on with the application with caching. When working and consuming the FactsService endpoint, for one endpoint, the factID one, I thought it would perform better if I cached the facts by ID. What I did here, I annotated with @CacheResult, my method. I'm caching the results of the Cat fact by ID asynchronously, using the cache key for factID.

Any good cache definition also requires more information like initial size capacity. For example, for this one, I defined an initial size capacity of 10. Let me change the name to animal-fact-async, the maximum size of 20, and the expire-after-write, so the time that the cache will expire to 60 seconds. When using a cache, you should manage the resources correctly to ensure that you're not introducing some memory leaks. Let's test the performance of our cache by using a gauge, another Micrometer metric, to see how our cache behaves. We're going to assess the cache size. I will first retrieve the cache and define the gauge with its details. Then I am going to execute some tests. I will have five requests to be executed against the FactsEndpoint and observe how the cache from FactsService is behaving. I need to measure how many cache facts I expect, regardless of five-time invocations. I expect that I will have three facts that my gauge will measure. Let's rerun the unit test and see how this is behaving for the cache.

Another approach that I want to showcase is the distribution of events and how your endpoints behave. Especially when you are having an intensive load of work. I like GraphQL for orchestration between the microservices because you can define schemas and work with those schemas. You can specify a lot easier integration points between applications. I defined my FactsEndpoint API as a GraphQL API. Looking at this API, I want to make sure that this API is working correctly with FactsService API. I want to see how the distribution of events goes on when integrating with FactsService.

Given the metric registry, I can register a DistributionSummary for these. I'm going to see the distribution of events against the invocation of getByTypeAsync from FactsService. Then I will record 100 times this size. I'm going to record also an outlier size for this one. I expect that I have 101 invocations, 100 plus 1. In the distribution summary, the total amount of the distribution of events is 100 on this size, plus one or the outlier size. I'm just going to run this one and see how these measurements were captured correctly. Another measurement you can do is using the percentiles to measure how things are behaving with your app, and you can have some percentiles and see how your app is behaving to specific points. This test is similar to the one above; the difference is that I'm using the distribution summary with percentiles and a Prometheus registry. I am measuring the percent of what's going on here. Same with an outlier size, so I'm recording 100 facts with the same size and an outlier size. I also want to assess how it's going on with the percentiles. My expectation, in this case, is that the result contains, at the 99 percentile, the outlier size. I can do that by taking a snapshot, attaining the percentile values, mapping them, and inspecting them for a better view.

Sometimes when you're working within your application, you realize that at the beginning of your app, you want to make sure that you are preparing something for your data, something before your application becomes available for users. To do this, you can define a class and annotate it with that @Startup. In this implementation, I prepared some data. I'm getting some facts, initial capacity to some value, and then storing that in my database. Let me inject these two missing services here. When looking into my application.properties, the initial capacity varies: dev is 100, while prod capacity is 500. That takes a lot of time. Think about tests. On tests, I don't need that much capacity. One approach is setting the initial dev capacity to 10 so it doesn't execute when loading the entire context. Another method, if you don't want this to be executed with specific profiles, is to use @IfBuildProfile annotation.

Takeaways

You can refine your log details per application profile and per package need. You can validate your endpoints' performance, and you can first see the capacity of your caches using timers, gauges, or you can control your endpoint response time with distribution summaries. If you want to inhibit the execution of specific long-running endpoints not needed in certain application profiles, you can use @IfBuildProfile. You can generate the GraphQL schema for each microservices and use that schema stitching for further client consumption. The code is available on my GitHub account, https://github.com/ammbra/stiching-facts.

Questions and Answers

Ruiz: Are you going to share the code because it's so concise? I loved that when you were explaining, and everything was very clear. That's something I also like about Quarkus. It allows a lot of functionality in a very small space. I think it's nice for applications.

Mihalceanu: I like Quarkus as well for many reasons. I'm happy to see that it is evolving. I'm keeping an eye on code.quarkus.io, searching for the available guide. I recently tried Liquibase Extension to evaluate it for a use case. For those looking for a way to evolve your data model, persist it in a relational database, and want to evolve your database gradually, you can work incrementally with Liquibase. Another option would be Flyway. I prefer Liquibase because I used it some years ago in a Spring Boot project. I used the Liquibase Maven plugin for generating SQL and incrementing the changes. I find it helpful that Quarkus has such an extension for working with Liquibase.

Some of the extensions are experimental; aren't you afraid of that?

I'm not afraid of experiments. I'm ready to evolve with these. We are engineers; we find solutions. The answer to, they're experimental; do not worry. I can say from my own experience that I needed to make a few tweaks while using a version of the GraphQL Generator plugin. At least from my point of view, as a developer who likes to have things generated and sometimes boilerplated, it is a good starting point than writing the schema by hand all by myself.

Any tips for using Quarkus with Maven or Gradle?

I'm using Maven most of the time. You can generate your code with Gradle as well. I'm always adding the extensions using the command line or as specified in the guides. Of course, you might find that you need libraries, like Lombok, for example, that are not part of any extension of Quarkus, and be aware of how you evolve those integrations in time. What made you decide to use gRPC instead of REST for the communication between services APIs?

REST is widely used. Most teams and team members accept REST better than working with gRPC. I had a situation with gRPC and the content types: on image upload support that was not there out of the box. When choosing a technology, it's about people because people like tools or widely adopted libraries with a lot of developer tooling. That being said, gRPC is suitable if your team is also ready to accept it and embrace it. Most of the time, I'm taking decisions altogether with folks I'm working with. It's not just me. Also, with Quarkus, it's the most common decision. We all like it. We will all agree with it and learn to develop with it. It's not something that I can decide for others.

Did you use a specific Quarkus extension for Spring-like annotations?

Do you mean to work with Autowired and all those Spring annotations? When I had to translate some work from Spring to Quarkus, I used some of the extensions available. One reason for going with Quarkus was more like combining the best of the two worlds. Quarkus is becoming more and more mature, but I'm also keeping an eye on the Spring ecosystem. If you're thinking of using Quarkus, try to POC first, have a good base on what you plan to do with it, and not just go big because somebody said it's fantastic. You need to have some measurements and see how that works for you. Of course, have the buy-in of everybody in the team, that they're ready to learn this, that they like this approach, that they would support it. In my opinion, when it comes to Quarkus, the learning curve is relatively easy".

 

See more presentations with transcripts

 

Recorded at:

Dec 16, 2021

BT