GraphQL — The Future of Microservices?
Today, we'll talk about how to use GraphQL with Spring Boot microservices and the advantages over REST APIs.
Join the DZone community and get the full member experience.
Join For FreeOften, GraphQL is presented as a revolutionary way of designing web APIs in comparison to REST. However, if you would take a closer look at that technology, you will see that there are so many differences between them. GraphQL is a relatively new solution that was open sourced by Facebook in 2015. Today, REST is still the most popular paradigm used for exposing APIs and inter-service communication between microservices. Is GraphQL going to overtake REST in the future? Let's take a look how to create microservices communicating through GraphQL API using Spring Boot and Apollo client.
Let's begin with an architecture of our sample system. We have three microservices that communicate with each other using URLs taken from Eureka service discovery.
1. Enabling Spring Boot Support for GraphQL
We can easily enable support for GraphQL in the server-side Spring Boot application by including some starters. After including graphql-spring-boot-starter
, the GraphQL servlet will be automatically accessible under the path /graphql
. We can override that default path with the settings property graphql.servlet.mapping
in the application.yml
file. We should also enable GraphiQL, an in-browser IDE for writing, validating, and testing GraphQL queries, and GraphQL Java Tools library, which contains useful components for creating queries and mutations. Thanks to that library, any files on the classpath with the .graphqls
extension will be used to provide the schema definition.
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.3</version>
</dependency>
2. Building a GraphQL Schema Definition
Every schema definition contains data type declarations, relationships between them, and a set of operations including queries for searching objects and mutations for creating, updating, or deleting data. Usually, we will start by creating a type declaration, which is responsible for domain object definition. You can specify if the field is required using !
or if it is an array using [...]
. The definition has to contain a type declaration or reference to other types available in the specification.
type Employee {
id: ID!
organizationId: Int!
departmentId: Int!
name: String!
age: Int!
position: String!
salary: Int!
}
Here's an equivalent Java class to the GraphQL definition visible above. The GraphQL type Int
can be also mapped to Java Long
. The ID
scalar type represents a unique identifier; in that case, it also would be Java Long
.
public class Employee {
private Long id;
private Long organizationId;
private Long departmentId;
private String name;
private int age;
private String position;
private int salary;
// constructor
// getters
// setters
}
The next part of the schema definition contains queries and mutations declarations. Most of the queries return a list of objects marked with [Employee]
. Inside the EmployeeQueries
type, we have declared all find methods, while inside the EmployeeMutations
type, methods for adding, updating, and removing employees. If you pass the whole object to that method, you need to declare it as an input
type.
schema {
query: EmployeeQueries
mutation: EmployeeMutations
}
type EmployeeQueries {
employees: [Employee]
employee(id: ID!): Employee!
employeesByOrganization(organizationId: Int!): [Employee]
employeesByDepartment(departmentId: Int!): [Employee]
}
type EmployeeMutations {
newEmployee(employee: EmployeeInput!): Employee
deleteEmployee(id: ID!) : Boolean
updateEmployee(id: ID!, employee: EmployeeInput!): Employee
}
input EmployeeInput {
organizationId: Int
departmentId: Int
name: String
age: Int
position: String
salary: Int
}
3. Queries and Mutation Implementation
Thanks to GraphQL Java Tools and Spring Boot GraphQL auto-configuration, we don't need to do much to implement queries and mutations in our application. The EmployeesQuery
bean has theGraphQLQueryResolver
interface. Based on that, Spring will be able to automatically detect and call the right method as a response to one of the GraphQL queries declared inside the schema. Here's a class containing an implementation of queries.
@Component
public class EmployeeQueries implements GraphQLQueryResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeQueries.class);
@Autowired
EmployeeRepository repository;
public List employees() {
LOGGER.info("Employees find");
return repository.findAll();
}
public List employeesByOrganization(Long organizationId) {
LOGGER.info("Employees find: organizationId={}", organizationId);
return repository.findByOrganization(organizationId);
}
public List employeesByDepartment(Long departmentId) {
LOGGER.info("Employees find: departmentId={}", departmentId);
return repository.findByDepartment(departmentId);
}
public Employee employee(Long id) {
LOGGER.info("Employee find: id={}", id);
return repository.findById(id);
}
}
If you would like to call, for example, the method employee(Long id)
, you should build the following query. You can easily test it in your application using the GraphiQL tool available under the path /graphiql
.
The bean responsible for implementation of mutation methods needs to implement GraphQLMutationResolver
. Despite the declaration of EmployeeInput
, we still use the same domain object as returned by queries: Employee
.
@Component
public class EmployeeMutations implements GraphQLMutationResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeQueries.class);
@Autowired
EmployeeRepository repository;
public Employee newEmployee(Employee employee) {
LOGGER.info("Employee add: employee={}", employee);
return repository.add(employee);
}
public boolean deleteEmployee(Long id) {
LOGGER.info("Employee delete: id={}", id);
return repository.delete(id);
}
public Employee updateEmployee(Long id, Employee employee) {
LOGGER.info("Employee update: id={}, employee={}", id, employee);
return repository.update(id, employee);
}
}
We can also use GraphiQL to test mutations. Here's the command that adds a new employee and receives a response with the employee's id and name.
4. Generating Client-Side Classes
We have successfully created a server-side application and we have already tested some queries using GraphiQL, but our main goal is to create another microservices that communicates with the employee-service
application through a GraphQL API. Here is where most tutorials about Spring Boot and GraphQL end.
To be able to communicate with our first application through a GraphQL API, we have two choices. We can get a standard REST client and implement the GraphQL API by ourselves with HTTP GET requests or using an existing Java client. Surprisingly, there are not many GraphQL Java client implementations available. The most serious choice is Apollo GraphQL Client for Android. Of course, it is not designed only for Android devices, and you can successfully use it in your Java microservice application.
Before using the client, we need to generate classes from the schema and.graphql
files. The recommended way to do this is through the Apollo Gradle Plugin. There are also some Maven plugins, but none of them provide the same level of automation as Gradle plugin. For example, it automatically downloads the Node.js required for generating client-side classes. So, the first step is to add the Apollo plugin and runtime to the project dependencies.
buildscript {
repositories {
jcenter()
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}
dependencies {
classpath 'com.apollographql.apollo:apollo-gradle-plugin:1.0.1-SNAPSHOT'
}
}
apply plugin: 'com.apollographql.android'
dependencies {
compile 'com.apollographql.apollo:apollo-runtime:1.0.1-SNAPSHOT'
}
The GraphQL Gradle plugin tries to find files with the .graphql
extension and schema.json
inside the src/main/graphql
directory. The GraphQL JSON schema can be obtained from your Spring Boot application by calling the resource /graphql/schema.json
. The file .graphql
contains queries definitions. The query employeesByOrganization
will be called by organization-service
, while employeesByDepartment
by both department-service
and organization-service
. Those two applications need a different set of data in the response. The application department-service
requires more detailed information about every employee than organization-service
. GraphQL is an excellent solution in that case because we can define the required set of data in the response on the client side. Here's query definition of employeesByOrganization
called by organization-service
.
query EmployeesByOrganization($organizationId: Int!) {
employeesByOrganization(organizationId: $organizationId) {
id
name
}
}
The application organization-service
would also call the employeesByDepartment
query.
query EmployeesByDepartment($departmentId: Int!) {
employeesByDepartment(departmentId: $departmentId) {
id
name
}
}
The query employeesByDepartment
is also called by department-service
, which requires not only id
and name
fields, but also position
and salary
.
query EmployeesByDepartment($departmentId: Int!) {
employeesByDepartment(departmentId: $departmentId) {
id
name
position
salary
}
}
All the generated classes are available under the build/generated/source/apollo
directory.
5. Building the Apollo Client With Discovery
After generating all required classes and including them in calling microservices, we may proceed to the client implementation. The Apollo client has two important features that will affect our development:
- It provides only asynchronous methods based on callback.
- It does not integrate with service discovery based on Spring Cloud Netflix Eureka.
Here's an implementation of the employee-service
client inside department-service
. I used EurekaClient
directly (1). It gets all running instances registered as EMPLOYEE-SERVICE
. Then it selects one instance from the list of available instances randomly (2). The port number of that instance is passed to ApolloClient
(3). Before calling the asynchronous method enqueue
provided by ApolloClient
, we create a lock (4) which waits a maximum of 5 seconds before releasing (8). The method enqueue
returns a response in the callback method onResponse
(5). We map the response body from the GraphQL Employee
object to the returned object (6) and then release the lock (7).
@Component
public class EmployeeClient {
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeClient.class);
private static final int TIMEOUT = 5000;
private static final String SERVICE_NAME = "EMPLOYEE-SERVICE";
private static final String SERVER_URL = "http://localhost:%d/graphql";
Random r = new Random();
@Autowired
private EurekaClient discoveryClient; // (1)
public List findByDepartment(Long departmentId) throws InterruptedException {
List employees = new ArrayList();
Application app = discoveryClient.getApplication(SERVICE_NAME); // (2)
InstanceInfo ii = app.getInstances().get(r.nextInt(app.size()));
ApolloClient client = ApolloClient.builder().serverUrl(String.format(SERVER_URL, ii.getPort())).build(); // (3)
CountDownLatch lock = new CountDownLatch(1); // (4)
client.query(EmployeesByDepartmentQuery.builder().build()).enqueue(new Callback() {
@Override
public void onFailure(ApolloException ex) {
LOGGER.info("Err: {}", ex);
lock.countDown();
}
@Override
public void onResponse(Response res) { // (5)
LOGGER.info("Res: {}", res);
employees.addAll(res.data().employees().stream().map(emp -> new Employee(Long.valueOf(emp.id()), emp.name(), emp.position(), emp.salary())).collect(Collectors.toList())); // (6)
lock.countDown(); // (7)
}
});
lock.await(TIMEOUT, TimeUnit.MILLISECONDS); // (8)
return employees;
}
}
Finally, EmployeeClient
is injected into the query resolver class DepartmentQueries
, and used inside the query departmentsByOrganizationWithEmployees
.
@Component
public class DepartmentQueries implements GraphQLQueryResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(DepartmentQueries.class);
@Autowired
EmployeeClient employeeClient;
@Autowired
DepartmentRepository repository;
public List departmentsByOrganizationWithEmployees(Long organizationId) {
LOGGER.info("Departments find: organizationId={}", organizationId);
List departments = repository.findByOrganization(organizationId);
departments.forEach(d -> {
try {
d.setEmployees(employeeClient.findByDepartment(d.getId()));
} catch (InterruptedException e) {
LOGGER.error("Error calling employee-service", e);
}
});
return departments;
}
// other queries
}
Before calling the target query, we should take a look at the schema created for department-service
. Every Department
object can contain the list of assigned employees, so we also define the type Employee
referenced by Department
type.
schema {
query: DepartmentQueries
mutation: DepartmentMutations
}
type DepartmentQueries {
departments: [Department]
department(id: ID!): Department!
departmentsByOrganization(organizationId: Int!): [Department]
departmentsByOrganizationWithEmployees(organizationId: Int!): [Department]
}
type DepartmentMutations {
newDepartment(department: DepartmentInput!): Department
deleteDepartment(id: ID!) : Boolean
updateDepartment(id: ID!, department: DepartmentInput!): Department
}
input DepartmentInput {
organizationId: Int!
name: String!
}
type Department {
id: ID!
organizationId: Int!
name: String!
employees: [Employee]
}
type Employee {
id: ID!
name: String!
position: String!
salary: Int!
}
Now we can call our test query with a list of required fields using GraphiQL. The application department-service
is by default available under port 8091, so we may call it using the address http://localhost:8091/graphiql
.
Conclusion
GraphQL seems to be an interesting alternative to standard REST APIs. However, we should not consider it a replacement for REST. There are some use cases where GraphQL may be the better choice, and some use cases where REST is the better choice. If your client does not need the full set of fields returned by the server side, and moreover, you have many clients with different requirements for a single endpoint, GraphQL is a good choice. When it comes to microservices, there are no solutions based on Java that allow you to use GraphQL together with service discovery, load balancing, or API gateway out-of-the-box. In this article, I have shown an example of using Apollo GraphQL clients together with Spring Cloud Eureka for inter-service communication. The sample application's source code is available on GitHub.
Published at DZone with permission of Piotr Mińkowski, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments