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:
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:
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.
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:
We need to create only one request with a description of data we need, and wait when GraphQL server collect all data for us.
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:
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...
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
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"
}
]
}
}
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.
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 PlantPropertyAPIimages
from PlantImageAPIIt 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.