Full Stack Blog – Federated API with GraphQL and Java

12 June 2023

Federated API with GraphQL and Java

What am I trying to do?

I want to combine existing APIs and provide simple way for clients to pull data in different use cases. At the same time, all separated services which I would like to use have common data model with unified and common identifiers (for example - plantId).

The federated architecture is reflected in the structure of the various identifiers and names used in the API.

Federated architecture is a pattern that unifies semi-autonomous applications, networks, or software systems. Each system operates semi-autonomously. It can scale, process, experiment, and implement different technology. However, it complies with the rules that allow it to exist symbiotically with other related systems (“Union”).

Federated API - every request is made by a service component in one backend and responded to by a service component in the other backend.

Services to build Federated API on top of them:

  • PlantSearchAPI - find plant by name. Input: search string; Output: list of plantId

  • PlantPropertyAPI - plant properties (color, height, etc...). Input: plantId; Output: PlantProperty

  • PlantImageAPI - plant images. Input: plantId; Output: list of image url's with metadata

And finally, I would like to have something like this:

federated api the goal

one entry point to get any data about plants.

Federated API should be able to handle requests to do search and get details for search results:

  1. First step. Request to search service and get list of plants.
  2. Second step. Load details for each plant.

GraphQL

Let's try to realize the desired with GraphQL. At first glance, GraphQL can do it.

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

taken from official site

Also, with GraphQL we can have very good documentation for API like OpenAPI for REST.

In Federated API GraphQL will work as suggested in sequence diagram below:

federated api graphql sequence

We need to create only one request with a description of data we need, and wait when GraphQL server collect all data for us.

GraphQL with Java

Spring has good solution for this - Spring for GraphQL.

Spring for GraphQL provides support for Spring applications built on GraphQL Java. It is a joint collaboration between the GraphQL Java team and Spring engineering. taken from docs

Spring for GraphQL supports:

  • http, websocket
  • Spring MVC, Spring WebFlux and RSocket
  • Spring Data to expose repository
  • Spring Security
  • OpenFeign to interact with downstream API's
  • Interceptors and ability to change request and responses
  • etc...

Implementation

This demo project was organized in gradle monorepo where all services and GraphQL implementation and shared models are in one repository. Source codes you can find here: https://github.com/AGanyushkin/demo-federatedapi

At the first step, to implement GraphQL we should define the description of data model for our Federated API service. In case of GraphQL with should create definition file in graphql/src/main/resources/graphql/plant.graphqls

type Query {
    search(keyword: String, limit: Int): [PlantSearchResultEntry]
    properties(plantId: String): [PlantProperty]
    description(plantId: String, limit: Int): [PlantTextFragment]
    images(plantId: String, limit: Int): [PlantImage]
}

enum PlantPropertyType {
    COLOR
    HEIGHT
}

type PlantSearchResultEntry {
    id: String,
    score: Float,

    plantTextFragments(limit: Int): [PlantTextFragment],
    properties: [PlantProperty]
    images(limit: Int): [PlantImage]
}

type PlantProperty {
    id: String,
    name: String,
    type: PlantPropertyType,
    value: String
}

type PlantTextFragment {
    id: String,
    text: String
}

type PlantImage {
    id: String,
    width: Int,
    height: Int,
    url: String
}

In this file we describe our federated data model. Here we have entities for PlantSearchResultEntry, PlantProperty, PlantTextFragment, PlantImage and top level query description:

type Query {

    # do search with keyword,limit arguments and return list of results
    search(keyword: String, limit: Int): [PlantSearchResultEntry]

    # get properties for plant, by plantId
    properties(plantId: String): [PlantProperty]

    # get text description for plant, by plantId
    description(plantId: String, limit: Int): [PlantTextFragment]

    # get images for plant, by plantId
    images(plantId: String, limit: Int): [PlantImage]
}

With enum type we make our model more strict. It is also possible to use string type instead of enum type.

enum PlantPropertyType {
    COLOR
    HEIGHT
}

Next and maybe most interesting part in this model:

type PlantSearchResultEntry {
    id: String,
    score: Float,

    plantTextFragments(limit: Int): [PlantTextFragment],
    properties: [PlantProperty]
    images(limit: Int): [PlantImage]
}

here, we have model for PlantSearchResultEntry with composition for plantTextFragments, properties, images fields. It allows as to get details for PlantSearchResultEntry but in original model from search service we don't have these fields:

// blog.fullstack.shared.search.PlantSearchResultEntry
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PlantSearchResultEntry {
    private UUID id;
    private double score;
}

Described composition for plantTextFragments, properties, images processed by GraphQL.

To make this processing possible, we must define mapping in Java code and describes how data will be resolved in external APIs:

// blog/fullstack/graphql/controller/graphql/PlantGraphQLController.java:55
@RequiredArgsConstructor
@Controller
public class PlantGraphQLController {
    private final PlantSearchApiClient searchClient;
    private final PlantPropertyApiClient propertyClient;
    private final PlantImageApiClient imageClient;

    @QueryMapping
    public List<PlantSearchResultEntry> search(@Argument String keyword,
                                               @Argument Integer limit) {
        return searchClient.doSearch(keyword, limit);
    }
    ...

    @SchemaMapping
    public List<PlantImage> images(PlantSearchResultEntry searchResultEntry,
                                   @Argument Integer limit) {
        return imageClient.getImages(searchResultEntry.getId(), limit);
    }

    // first argument in function `images` 
    //      - `PlantSearchResultEntry searchResultEntry` it is current element from parent type
    // other arguments which annotated with `@Argument`
    //      can be passed from GraphQL query
}

QueryMapping - it is mapping for top level queries

SchemaMapping - describes how composition for fields plantTextFragments, properties, images will be resolved.

Clients for this demo it is simple FeignClient's

@FeignClient(value = "plantPropertyAPIClient", url = "${downstream.plantpropertyapi.url}")
public interface PlantPropertyApiClient {

    @RequestMapping(method = RequestMethod.GET, value = "/plant/property")
    List<PlantProperty> getProperties(@RequestParam UUID plantId);

    @RequestMapping(method = RequestMethod.GET, value = "/plant/description")
    List<PlantTextFragment> getTextFragments(@RequestParam UUID plantId,
                                             @RequestParam(required = false) Integer limit);
}

Of course, this solution in real life will be more flexible with services etc...

Review result and send requests

Let's try to send requests. And for that we have very nice UI with sandbox and documentation.

In demo implementation you can use it here: http://localhost:8170/graphiql?path=/graphql

GraphQL UI sandbox

Request data from downstream APIs

We can request data from any external API. Let's try to request properties for plant with id="b5cdece0-cabf-4a8e-abfb-8a0ecebb4884":

# request
query propertiesWithAllFields {
    properties(plantId: "b5cdece0-cabf-4a8e-abfb-8a0ecebb4884") {
        id,
        name,
        type,
        value
    }
}

# response
{
  "data": {
    "properties": [
      {
        "id": "6a8519f8-6596-4a72-b412-ea8a5a918d24",
        "name": "leaf color",
        "type": "COLOR",
        "value": "blue"
      },
      {
        "id": "745fd8fe-c803-49fe-be6b-3957d42673b3",
        "name": "root color",
        "type": "COLOR",
        "value": "plum"
      },
      {
        "id": "a0fe6caf-c544-4291-a97a-c9050bd1b0b8",
        "name": "max height",
        "type": "HEIGHT",
        "value": "17.548228656731887"
      }
    ]
  }
}

if it is required, we can choose which fields in response will be returned:

# request
query propertiesWithTwoFields {
    properties(plantId: "b5cdece0-cabf-4a8e-abfb-8a0ecebb4884") {
        type,
        value
    }
}

# response
{
  "data": {
    "properties": [
      {
        "type": "COLOR",
        "value": "lavender"
      },
      {
        "type": "COLOR",
        "value": "blue"
      },
      {
        "type": "HEIGHT",
        "value": "11.54574071950894"
      }
    ]
  }
}

Request data from different API in one GraphQL request

It is not a real composition, we just request data from two downstream APIs in one GraphQL request:

# request
query detailsForPlant($plantId: String!) {
    properties(plantId: $plantId) {
        type,
        value
    }
    description(plantId: $plantId, limit:2) {
        text
    }
}

# request variables
{
  "plantId": "b5cdece0-cabf-4a8e-abfb-8a0ecebb4884"
}

# response
{
  "data": {
    "properties": [
      {
        "type": "COLOR",
        "value": "black"
      },
      {
        "type": "COLOR",
        "value": "lime"
      },
      {
        "type": "HEIGHT",
        "value": "4.788214923145635"
      }
    ],
    "description": [
      {
        "text": "Hard as this may be to believe, it’s possible that I’m not boyfriend material."
      },
      {
        "text": "Scissors cuts paper, paper covers rock, rock crushes lizard, lizard poisons Spock, Spock smashes scissors, scissors decapitates lizard, lizard eats paper, paper disproves Spock, Spock vaporizes rock, and as it always has, rock crushes scissors."
      }
    ]
  }
}

In data section in results we have two different sections properties and description, it is results from different APIs.

Do search and return entries with additional details

Let's try to do search and enrich search results with data from other APIs.

# request
query detailedSearchResults($keyword:String!, $limit:Int!) {
    search(keyword: $keyword, limit: $limit) {
        id,
        score,
        properties {
            type,
            value
        },
        plantTextFragments(limit:1) {
            text
        },
        images(limit:1) {
            width,
            height,
            url
        }
    }
}

# request variables
{
  "keyword": "betula",
  "limit": 1
}

# response
{
  "data": {
    "search": [
      {
        "id": "e2999c8e-c4f9-43f3-8d79-d7470f0b0c06",
        "score": 0.9813466408192897,
        "properties": [
          {
            "type": "COLOR",
            "value": "black"
          },
          {
            "type": "COLOR",
            "value": "lavender"
          },
          {
            "type": "HEIGHT",
            "value": "18.382231394413306"
          }
        ],
        "plantTextFragments": [
          {
            "text": "I would have been here sooner but the bus kept stopping for other people to get on it."
          }
        ],
        "images": [
          {
            "width": 1483,
            "height": 1326,
            "url": "www.val-reinger.org"
          }
        ]
      }
    ]
  }
}

In results, we have fields from PlantSearchAPI: id and score and fields from two other APIs:

  • properties from PlantPropertyAPI,
  • plantTextFragments from PlantPropertyAPI
  • images from PlantImageAPI

Conclusion

It is possible to create Federated API with GraphQL. GraphQL provides ability to implement composition without changes in existing APIs, but any changes in external APIs requires changes in Federated API service. To try to avoid change is Federated API service we can generate and load graphql model definition from external source, but graphql model definition should be regenerated when external api is changed. It means we can't create schemaless implementation for Federated API service. With Spring for GraphQL service we can use Spring features, and it is great ability.