Distributed application and GraphQL
How to code a distributed application with multiple microservices and connect them all with GraphQL.
Studying the implementation of GraphQL, I realized that it’s possible to use that technology to spread the business model across multiple microservices and let the API do the heavy lifting for the integration.
I built a simple demo application to put the theory into practice. The use case is, of course, pretty straightforward: there are countries and countries contains cities. As a user, I want to search countries, cities, cities from a country, and a country from a city.
The next step is to define my services. I opted for three nodes: City Service, Country Service, and a Frontend Node to integrate them all. City Service and Country Service never interact with each other, and Frontend Node is the only one aware of the complete model.
All the code is available in https://github.com/alros/distributed-graphql, and here I’ll focus on only a few parts.
First data service
I’ll start from Country Service. The first step is to create a basic Spring Boot application with these two modules:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-web is essential to expose the API via HTTP. The next step is to add the dependencies for GraphQL:
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>11.1.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>11.1.0</version>
</dependency>
Please note that my strategy for the API definition is contract-first because I find it easier. It’s always possible to work code-first.
By default, the application will look for descriptors in resources/graphql. I created schema.graphqls with the following definition:
type City {
id:ID!
name:String!
countryId:String!
}
type Query {
allCities:[City]
city(id:String!):City
citiesInCountry(countryId:String!):[City]
}
If you are unfamiliar with the language, this defines the type City for the data and the special type Query that contains the accepted queries.
- allCities returns an array of cities (City is in [..])
- city accepts a parameter id (mandatory because there’s a “!”) and returns a City
- citiesInCountry accepts a parameter countryId and returns an array of cities.
The code part is heavily supported by coding conventions to automatically wire the schema to the code. The framework will look for a pojo called “City” to map the type and a class Query implementing GraphQLQueryResolver to map all the queries.
@Component
public class Query implements GraphQLQueryResolver {
private static final Logger LOG = LoggerFactory.getLogger(Query.class);
@Autowired
private CityRepository countryRepository;
public List<City> allCities() {
LOG.info("allCities");
return countryRepository.getAllCities();
}
public City city(String id) {
LOG.info("city {}", id);
return countryRepository.getCity(id);
}
public List<City> citiesInCountry(String countryId) {
LOG.info("citiesInCountry {}", countryId);
return countryRepository.citiesInCountry(countryId);
}
}
All methods are the translation in Java of the defined queries. This Query component is the entry point of the City Service, and, from here, it’s possible to implement whatever logic is needed to expose the data. In my example, I simply hardcoded some stuff in the repositories.
And… that’s it! It’s now possible to start the application and interact with the service via http://localhost:8080/graphiql
Using GraphQL, the user describes the expected model. For example:
{
city(id:"1"){
name
}
}
will return
{
"data": {
"city": {
"name": "Turin"
}
}
}
while
{
city(id:"1"){
id
name
countryId
}
}
will return
{
"data": {
"city": {
"id": "1",
"name": "Turin",
"countryId": "1"
}
}
}
It’s all handled by the framework and it will be important later.
Second data service
Next step is to code Country Service. I’ll skip the description here since it’s just like City Service with a different model. A note. By default Spring Boot starts on 8080, so I configured the two data services on 8090 and 8091. This may not be important working with containers, for example.
Frontend node — API
When the data services are up and running, it comes the time of the Frontend node. This node presents the additional challenge of interacting with the GraphQL API exposed by the data services.
In the pom of Frontend, Spring Boot and GraphQL are just like the data services:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>11.1.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>11.1.0</version>
</dependency>
Next step is to define the schema of the API exposed to the user.
type City {
id:ID!
name:String!
country:Country!
}type Country {
id:ID!
name:String!
cities:[City]
}type Query {
allCountries:[Country]
allCities:[City]
city(id:String!):City
country(id:String!):Country
}
The definition is a sort of merge of the two data services, but there are two huge differences: in City, country is of type Country, and in Country cities is an array of type City.
The user will be able to perform a query like the following:
query{
country(id:"1"){
name
cities{
name
}
}
}
It will return a richer data model that mixes the two data models.
{
"data": {
"country": {
"name": "Italy",
"cities": [
{
"name": "Turin"
},
{
"name": "Milan"
},
{
"name": "Rome"
}
]
}
}
}
Let’s explore the mapping.
Just like in City Service, there will be the classes City and Country to model the types, but the Java implementation will still use the primary-key-foreign-key approach:
public class City {
private String id;
private String name;
private String countryId;
...
}
and
public class Country {
private String id;
private String name;
...
}
There’s the usual class Query to map the queries:
@Component
public class Query implements GraphQLQueryResolver {
private static final Logger LOG = LoggerFactory.getLogger(Query.class);
@Autowired
private CountryService countryService;
@Autowired
private CityService cityService;
public List<Country> allCountries() {
LOG.info("allCountries");
return countryService.getAllCountries();
}
public List<City> allCities() {
LOG.info("allCities");
return cityService.getAllCities();
}
public Country country(String id) {
LOG.info("country {}", id);
return countryService.getCountry(id);
}
public City city(String id) {
LOG.info("city {}", id);
return cityService.getCity(id);
}
}
Up to here, it’s the same as the data services, and for a simple query such as the following, it may even be enough.
query{
country(id:"1"){
name
}
}
However, if the user queries all the cities in a country, GraphQL needs a way to resolve the relation. The new component to be introduced is a GraphQLResolver.
@Component
public class CitiesResolver implements GraphQLResolver<Country> {
private static final Logger LOG = LoggerFactory.getLogger(CitiesResolver.class);
@Autowired
private CityService cityService;
public List<City> cities(Country country) {
LOG.info("cities {}", country);
return cityService.getCitiesInCountry(country.getId());
}
}
The resolver maps the relation with a method called “cities” that accepts a Country. The method is called automatically by the framework when the user requests the field “cities”.
The mechanism works in the same way when the user requests the country of a given city:
query{
city(id:"1"){
name
country{
name
}
}
}
GraphQL will search for a GraphQLResolver returing a Country given a City:
@Component
public class CountryResolver implements GraphQLResolver<City> {
private static final Logger LOG = LoggerFactory.getLogger(CountryResolver.class);
@Autowired
private CountryService countryService;
public Country country(City city) {
LOG.info("country {}", city);
return countryService.getCountry(city.getCountryId());
}
}
At this point the interaction user / Frontend is ready. What’s missing is the integration Frontend / data services.
Frontend node — Integration with data services
There are a few different options to call a GraphQL server from a Java application, but all have some disadvantages. I opted for Apollo Android that, despite the name, is not specific for Android. It seems one of the most complete and supported with the advantage of offering type-safe API, but it’s not exactly the easiest framework to use.
The dependency is:
<dependency>
<groupId>com.apollographql.apollo</groupId>
<artifactId>apollo-runtime</artifactId>
<version>2.5.9</version>
</dependency>
Apollo also requires a plugin:
<plugin>
<groupId>com.github.aoudiamoncef</groupId>
<artifactId>apollo-client-maven-plugin</artifactId>
<version>4.0.1</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<skip>false</skip>
<services>
<countryService>
<introspection>
<enabled>true</enabled>
<endpointUrl>http://localhost:8090/graphql</endpointUrl>
<prettyPrint>true</prettyPrint>
</introspection>
<sourceFolder>${project.basedir}/src/main/graphql/countryService</sourceFolder>
<compilationUnit>
<name>countryService</name>
<outputDirectory>${project.build.directory}/generated-sources/countryService</outputDirectory>
</compilationUnit>
</countryService>
<cityService>
<introspection>
<enabled>true</enabled>
<endpointUrl>http://localhost:8091/graphql</endpointUrl>
<prettyPrint>true</prettyPrint>
</introspection>
<sourceFolder>${project.basedir}/src/main/graphql/cityService</sourceFolder>
<compilationUnit>
<name>cityService</name>
<outputDirectory>${project.build.directory}/generated-sources/cityService</outputDirectory>
</compilationUnit>
</cityService>
</services>
</configuration>
</execution>
</executions>
</plugin>
What is this plugin? As those familiar with SOAP Webservices may guess, Apollo parses the graphql schema of the services to generate code and expose type-safe API to remote services. I’m not a big fan of this, but it works pretty well once the setup is in place.
The plugin in the pom.xml will look for descriptors of the queries that the project must integrate. Descriptors must be located in src/main/graphql/serviceName, so, in this case, in src/main/graphql/cityService and src/main/graphql/countryService.
CityService.graphql will look like this:
fragment CityFragment on City {
id
name
countryId
}
query GetAllCities {
allCities{
...CityFragment
}
}
query GetCity($id:String!) {
city(id:$id) {
...CityFragment
}
}
query GetCitiesInCountry($countryId:String!) {
citiesInCountry(countryId:$countryId) {
...CityFragment
}
}
CityFragment is a sort of reusable type. Defining fragments is useful because it reduces duplication in the generated code. Each query will be translated in a class, and the parameters will become the signatures of the constructors.
In Frontend, Query uses CityService and CountryService to retrieve the data and these two classes have the responsibility of interacting with the data services. Let’s explore CityService.
First of all, the class needs an Apollo Client:
@PostConstruct
public void init() {
apolloClient =
ApolloClient.builder().serverUrl(serverUrl).build();
}
Then there are the methods to retrieve the data, and this is where it gets convoluted, unfortunately.
public City getCity(String id) {
RemoteServerCallback<GetCityQuery.Data, City> callback;
Function<Data, City> mapper = data ->
map(data.city().fragments().cityFragment());
callback = new RemoteServerCallback<>(mapper);
return performQuery(callback, new GetCityQuery(id));
}
The “new GetCityQuery(id)” in the last line comes from the generated code. The rest is mostly my code wrapping Apollo’s code to perform the query and parse the response in the callback. Apollo works with callbacks and RemoteServerCallback is my extension of ApolloCall.Callback.
public class RemoteServerCallback<D extends Operation.Data, T> extends ApolloCall.Callback<D> {
private static final Logger LOG = LoggerFactory.getLogger(RemoteServerCallback.class);
private final CompletableFuture<T> value = new CompletableFuture<>();
private final Function<D, T> mapper;
public RemoteServerCallback(Function<D, T> mapper) {
this.mapper = mapper;
}
@Override
public void onResponse(@NotNull Response<D> response) {
value.complete(mapper.apply(response.getData()));
}
@Override
public void onFailure(@NotNull ApolloException e) {
value.cancel(true);
LOG.error("error in server callback", e);
}
public Future<T> getResult() {
return value;
}
}
It’s not necessary to follow my implementation on this aspect, but I find this solution easier to avoid juggling API calls with asynchronous callbacks.
The missing part is the method “performQuery” that I’ve hidden under the carpet given the amount of Generics that makes it unreadable.
protected <R, D extends Operation.Data, V extends Operation.Variables, Q extends Query<D, D, V>> R performQuery(
RemoteServerCallback<D, R> callback, Q query) {
apolloClient.query(query).enqueue(callback);
return getResult(callback);
}private <R, D extends Operation.Data> R getResult(RemoteServerCallback<D, R> callback) {
try {
return callback.getResult().get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
Test
Finally, it’s time to test the Frontend node and see what happens.
query{
allCountries{
name
}
}
It returns the following:
{
"data": {
"allCountries": [
{
"name": "Italy"
},
{
"name": "Greece"
}
]
}
}
This will result in this logging:
frontend:
Query: allCountriescountry:
Query: allCountries
Frontend calls Country Service once. No involvment of City Service.
Another case. Like above, but I also want the cities:
query{
allCountries{
name
cities{
name
}
}
}
It returns the following:
{
"data": {
"allCountries": [
{
"name": "Italy",
"cities": [
{
"name": "Turin"
},
{
"name": "Milan"
},
{
"name": "Rome"
}
]
},
{
"name": "Greece",
"cities": [
{
"name": "Athens"
},
{
"name": "Ioannina"
}
]
}
]
}
}
This will result in this logging:
frontend:
Query: allCountries
CitiesResolver: cities Country [id=1, name=Italy]
CitiesResolver: cities Country [id=2, name=Greece]country:
Query: allCountriescity:
Query: citiesInCountry 1
Query: citiesInCountry 2
Frontend calls Country Service to retrieve all countries, then, for each Country, it calls City Service to retrieve all the cities in the given country
Final case. Given a City’s id, I want the City’s name and the name of its country:
query{
city(id:"1"){
name
country{
name
}
}
}
This returns:
{
"data": {
"city": {
"name": "Turin",
"country": {
"name": "Italy"
}
}
}
}
With logging:
frontend:
Query: city 1
CountryResolver: country City [id=1, name=Turin, countryId=1]city:
Query: city 1country:
Query: country 1
Frontend calls City Service to retrieve the City with id 1, then, via CountryResolver, it calls Country Service to translate the City’s countryId into a Country.
Final considerations
GraphQL and this design look like a viable solution to integrate a complex model while keeping the single services separate and focused.
GraphQL drastically reduces the number of calls from the user because a single query can return the equivalent of many REST calls. However, multiple queries are just hidden inside the system. It appears clear from this example that the number of internal calls can explode in the absence of controls to prevent it.
The GraphQL framework to expose API is easy to use and requires minimal work. On the other end, Apollo is a reminiscence of an almost forgotten past writing ant-tasks to generate code from WSDL and integrate poorly named classes into the application.
Links
- My code: https://github.com/alros/distributed-graphql
- GraphQL: https://graphql.org/learn/schema/
- GraphQL Kickstart: https://www.graphql-java-kickstart.com/
- Apollo: https://www.apollographql.com/docs/android/essentials/get-started-java
- Apollo’s maven plugin: https://github.com/aoudiamoncef/apollo-client-maven-plugin
- Diagrams made with https://yuml.me/