Designing APIs with GraphQL + Project Reactor + Caffeine
Back in the days, when I started to dig into Reactive Programming and GraphQL, I realized there was no support yet to integrate both technologies natively, so I came up with a solution that might be useful in certain use cases like the one I am about to describe.
This PoC is all about retrieving lists of airports, countries and cities as if this were an airline. I have found the endpoints to retrieve the info from by easily doing some small research on the Google Chrome Developer Tools.
The whole code of the project can be found here: https://github.com/sergiosheypol/GraphQL_Reactor_BFF
GraphQL can be a nice technology when it comes to implementing Backend for Frontend microservices but Java can be tough sometimes (what a surprise!) with such a level of flexibility. Anyway, I found it interesting because it is very very easy to implement a GraphQL-based API with Spring Boot. As I will be explaining below, it is all about implementing a couple of interfaces and… voilà.
I simply love Project Reactor. I cannot add much more to that. I consider it extremely useful and efficient when in high load systems.
I will not be explaining the basics of GraphQL but how to integrate it with Spring Boot and Project Reactor so I advise you to have a look at other sources before jumping into this one.
Why use a cache?
Here it is the not-that-funny part: GraphQL Java Tools cannot resolve/subscribe to Project Reactor streams. So one fast solution would simply be using Project Reactor subscribe() or block() to retrieve the content of the stream… But here is when the trade-off solution comes in: a cache.
By using a cache, we (kind of) get the best of both worlds. We are not blocking the thread because we are being reactive/async and we have a way to give GraphQL the entities/models it is able to understand. The underlying idea is to check whether the value has already been cached and ask for it asynchronously otherwise.
There is a clear downside in this approach: what happens when the cache has not yet been populated? Well… It is simply going to make the first ever request slower but then everything will work fast.
Project Reactor is not playing any role unless the cache is empty. The most time consuming part in any application is the integration with third parties (databases, microservices, etc) so by making this part reactive, we are improving the performance of that bottleneck.
For this PoC, I will be using the following libraries:
Defining the allowed queries and mutations must be done inside a schema.graphqls file so let’s start by having a look at it:
There are six queries defined. Three of them take a parameter.
As a consequence, we must define our models to match the ones defined in this file… So there must be models called Airport.java, Coordinates.java, Country.java and City.java.
To simplify the example, I am not mapping them throughout the layers of the app. I should have used DTOs, entities and so on but this is simpler and faster.
Another important detail is that ‘Airport’ contains ‘Coordinates’, which is a custom scalar. That is going to force us to write a custom resolver for that model.
Fetching info from sources
I have used WebClient to connect to remote endpoints to bring the info as it natively returns Mono and Flux items:
Configuring in-memory caches
For this PoC, I am using Caffeine cache. Since we are defining three models (airport, city and country), we are using three caches. Here it is one of them:
To endow it with more features, I have wrapped it around a custom class, InMemoryCache, which simply takes a Caffeine cache and performs certain operations on it, like get(…), clear(…)
So to create our final cache product (wow!) we just need to instantiate beans of our amazing In-Memory cache as in the example in the bottom of the previous gist.
Business logic. Caching info
In this case, our business logic must handle the cache population and the decision of extracting items from them when it is populated. It needs to use the provider layer to do so.
I decided to create a generic service layer to avoid duplicating code:
This generic class looks quite complex but the idea underneath is simple:
- getAll(): checks whether the cache is not empty and returns it as list. In the case it is empty, it calls the provider layer and populates it.
- getOne(…): checks whether the cache is not empty and retrieves the value. If the cache is not empty and the value does not exist, it returns an empty object, but in the case that the cache is empty, it simply calls the doGetAll() method to populate it entirely. By doing this, the cache is always consistent and full.
The rest of the methods must be implemented by the child classes. For example, the AirportService class:
Exposing our API to the world — Resolver layer
And… last but not least, the resolver. This is the way GraphQL exposes the API. As mentioned above, this is very easy to do in Java by simply implementing the GraphQLQueryResolver:
This will automatically match the queries defined in your schema.graphqls. Do not worry about the method names… If it is not able to resolve them on its own, the app will simply crash at startup and suggest you the names it is trying to reach.
Coordinates Resolver — Creating our custom resolver
As mentioned above, Coordinates is a custom type, so the app will crash when trying to resolve it. We must define our own resolver. Do not worry about this… If it is needed, the app will simply ask for it. The idea is the same, we must implement an interface (GraphQLResolver<…>) and parametrize it:
The app will work its magic to link it (and will crash if the names are wrong, always suggesting the fix). In this case, the coordinates are in the Airport model, so we need to take it as a parameter.
Running the app
If you are here, it means your app is already set up, so congrats! Jokes aside, this is a normal Spring Boot app, so simply run it.
The default endpoint is /graphql and you can use GraphiQL in /graphiql because we have included it in our pom.xml
In this guide, we have learned how to create our Spring Boot app with GraphQL and Project Reactor. Despite they are not compatible, we have placed a Caffeine cache to avoid blocking the thread.