GraphQL with Spring Boot for Resource Aggregation

GraphQL is a API query language which allows clients to define exactly what they want in the response by invoking an API. On the server side, a runtime engine is used to execute queries to return the data requested by the clients.

This article will demonstrate how GraphQL can be used to implement a composite service API that aggregates data from multiple services. Composite service is a common pattern in service based architectures such as Microservices and Service Oriented Architecture (SOA). For example, a Ecommerce company may want to aggregate its various information about their products such as pricing, stock levels, sales and purchase order data into a single API for consumption by its own internal and 3rd party systems.

What we are building

In this blog, we are going to implement a product API that aggregates various product related information from individual systems and return the relevant data to the client based on the GraphQL query in the request. The diagram below shows the conceptual system architecture

blog-GraphQL (2)

Client applications request via GraphQL product information using the Aggregated Product Service. Based on the GraphQL query, the Aggregated Product Service gathers the required information from the relevant service(s) and returns the result to the client.

Project Setup

To implement the Aggregated Product Service, create a Spring Boot app with the following dependencies. GraphQL support is enabled via Spring Boot starters provided here.

<properties>
     <java.version>1.8</java.version>
     <graphql-spring-boot.version>5.11.1</graphql-spring-boot.version>
     <graphql-java.version>5.7.1</graphql-java.version>
</properties>

<dependencies>
     <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     <dependency>
        <groupId>com.graphql-java-kickstart</groupId>
        <artifactId>graphql-spring-boot-starter</artifactId>
        <version>${graphql-spring-boot.version}</version>
     </dependency>
     <dependency>
        <groupId>com.graphql-java-kickstart</groupId>
        <artifactId>graphiql-spring-boot-starter</artifactId>
        <version>${graphql-spring-boot.version}</version>
     </dependency>
     <dependency>
        <groupId>com.graphql-java-kickstart</groupId>
        <artifactId>graphql-java-tools</artifactId>
        <version>${graphql-java.version}</version>
     </dependency>
</dependencies>

The graphql-java-tool library allows use of the GraphQL schema language to build
the graphql-java schema. It parses the given GraphQL schema and allows you to BYOO (bring your own object) to fill in the implementations.

GraphQL Schema

GraphQL schema is defined using the Schema Definition Language (SDL) which specifies all the fields and operations exposed by the APIs. It is based around a type system.

Include the following scheme as file product.graphqls under src/main/resources/graphql folder. By default, the GraphQL starter scans GraphQL schema files from classpath folders **/*.graphql

# File src/main/resources/graphql/product.graphqls
type Product {
   sku: String
   stock: [Inventory!]!
   purchaseOrders: [PurchaseOrder!]!
   salesOrders: [SalesOrder!]!
}

type Inventory {
   location: String
   qtyAvailable: Int
}

type PurchaseOrder {
   orderDate: String
   shipDate: String
   qty: Int
}

type SalesOrder {
   item: Product!
   qty: Int
   unitPrice: Float
   totalAmount: Float
}

type Query {
   findProduct(sku:String): Product
}

GraphQL by default supports the scalar types: Int, Float, String, Boolean and ID.

[] is used to indicate an array and ! marks the attribute as Non-Null. For example

Product {
  ...
  stock: [Inventory!]!

means the stock attribute should be a Non-Null list of Inventory and each Inventory is also Non-Null.

The type Query is special in that it defines the entry point of every GraphQL queries. In our case, there is only 1 query findProduct

Data Classes

The types defined in the schema can be implemented as POJOs. For example

// Product.java
public class Product {
     private String sku;
     private List<Inventory> stock;
     private List<SalesOrder> salesOrders; 
     private List<PurchaseOrder> purchaseOrders;
     ...

Resolvers

The GraphQL Java Tools provides the GraphQLQueryResolver interface for implementing the queries.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import au.com.arbco.graphqlspike.model.Product;
import au.com.arbco.graphqlspike.service.ProductService;

@Component
public class QueryResolver implements GraphQLQueryResolver {

     @Autowired
     private ProductService productService;

     public Product findProduct(String sku) {
         return productService.getProduct(sku);
     }
}

Since we define the query findProduct in the schema, we implement the method with the same name here. If we implements the aggregated API as REST, it would fetch the various data from the corresponding services (inventory, sales, purchase), aggregate them into the Product and return it in the response. With GraphQL, fetching complex (non scalar) attributes is lazy and is only invoked when needed, i.e. the incoming query requests the attribute to be returned.

To resolve complex attributes of a type, the Java Tools provides the GraphQLResolver interface. The codes below demonstrate how to resolve the inventory, sales and purchase order attributes of the product type

@Component
public class ProductResolver implements GraphQLResolver<Product> {
     private Logger logger = LoggerFactory.getLogger(getClass());
     @Autowired
     private ProductService productService;

     public Inventory[] stock(Product product) {
          logger.info("Get inventory data for product: {}", product.getSku());
          return productService.getInventories(product);
     }

     public PurchaseOrder[] purchaseOrders(Product product) {
          logger.info("Get POs for product: {}", product.getSku());
          return productService.getPurchaseOrders(product);
     }

     public SalesOrder[] salesOrders(Product product) {
          logger.info("Get SOs for product: {}", product.getSku());
          return productService.getSalesOrders(product);
     }
}

In our scenario, the data is retrieved via REST APIs from the individual services. Below is a mock up implementation of the class ProductService

@Service
public class MockProductService implements ProductService {
     @Autowired
     private RestTemplate restTemplate;

     // ... URLs to services

     @Override
     public Inventory[] getInventories(Product product) {
          return restTemplate.getForObject(inventoryUrl, Inventory[].class, product.getSku());
     }

     @Override
     public PurchaseOrder[] getPurchaseOrders(Product product) {
          return restTemplate.getForObject(poUrl, PurchaseOrder[].class, product.getSku());
     }

     @Override
     public SalesOrder[] getSalesOrders(Product product) {
          return restTemplate.getForObject(soUrl, SalesOrder[].class, product.getSku());
     }

     @Override
     public Product getProduct(String sku) {
          Product product = new Product();
          product.setSku(sku);
          return product;
     }
}

Testing

The GraphiQL UI tool (included in the Maven dependencies) can be used for testing the server. Start up Spring Boot server as usual and go to link /graphiql (e.g. http://localhost:8080/graphiql) in the browser.

graphiql

To get product inventory only

Type the following query in the left panel:

query {
     findProduct(sku: "sku001") {
         sku
         stock {
            location
            qtyAvailable
         }
     }
}

will return the following results in GraphiQL

graphiql-stock

Note only the inventory API (method stock in the ProductResolver) is invoked. Below is the log message from the server

2020-07-01 14:04:15.506 INFO 13744 --- [io-8080-exec-10] a.c.a.g.resolver.ProductResolver : Get inventory data for product: sku001

To get sales orders only

Type the following query in the left panel:

query {
     findProduct(sku: "sku001") {
          sku
          salesOrders {
              qty
              totalAmount
          }
     }
}

will return the following results in GraphiQL

graphiql-order

Note the query only requests some of the sales order attributes (qty and totalAmount).

To get all product attributes

Type the following query in the left panel:

query {
     findProduct(sku: "sku001") {
          sku
          stock {
               location
               qtyAvailable
          }
          purchaseOrders {
               shipDate
               qty
          }
          salesOrders {
               qty
               totalAmount
          }
     }
}

will return the following results in GraphiQL

graphiql-po

Conclusions

GraphQL seems to be a good choice to implement resource aggregation to expose a higher level API using resources from individual APIs. If design properly, it could provide some performance improvement. Another advantage compared with REST is versioning. For example, adding new attributes to an API would not affect any existing clients as the responses to them would not be changed (as it is specified in the query). However, GraphQL also has some limitations so careful considerations should be made when applying it in different context.