The story is a well-known Not Invented Here syndrome among developers. Let me summarize from the beginning. Reverse proxies are good especially if you are using REST API, Even if you have multiple microservices behind the proxy. You can efficiently serve all your REST API via using an API Gateway. You can even make some transformation magic, authorization, aggregation, and plenty of other custom flavors. The sky is the limit.
But when it comes to GraphQL, it's not that easy. Especially if you want to transform a legacy code base without changing thousands of lines in frontend. You have 2 options one of them is GraphQL Federation which is a good practice to go with it. But it combines the methods in namespaces, so you might need some refactoring on frontend as well. The second option is GraphQL stitching.
I chose the stitching option. Then I stuck with Atlassian Braid which is a really handy tool TBH. But it seems deprecated and has not been touched for a while. Especially issues sections, there are plenty of bugs reported or questions from 3 years back. I was also trying to use Atlassian Braid on Spring Cloud Gateway. It was really good at the beginning till we deployed it on production. Because of the complex reactive streams logic on the Gateway. It causes lots of thread starvation or unresponsiveness. That eventually triggered me to implement a new one.
Like most of the developers, my estimation was wrong. I've estimated it as a weekend project then it turns out a complete level. then I ended up with Lilo Stitching library and here we are.
How GraphQL Stitching works
When the Stitching library starts, it fetches the schema from the target schema sources, They might be a remote schema source or an internal source as well. After that, it maps all the GraphQL mutations and queries with respective sources.
For every incoming request, it analyzes the query and redirects the requests to respective schema sources after that it combines all the results and returns an aggregated result as a GraphQL response.
Example usage
Installation
Required dependency should be added to your project if you're using maven
then you can add it into your pom.xml
file:
<dependencies>
...
<dependency>
<groupId>io.fria</groupId>
<artifactId>lilo</artifactId>
<version>23.11.1</version>
</dependency>
...
</dependencies>
If you're using gradle
then you can add the dependency in your build.gradle
file.
implementation 'io.fria:lilo:23.11.1'
Creating the source applications
You can use lilo with various libraries and web frameworks but I'll try to show a basic usage with spring boot. We need to create two different service projects for exhibiting lilo's skills. Thanks to GraphQL spring project we have now the ability to create GraphQL controller via using annotations just like the REST API.
Controller for Service Application1:
@Controller
public class GreetingController {
@QueryMapping
public @NonNull String greeting1() {
return "Hello world";
}
}
Then we are going to create a similar controller for our second application:
@Controller
public class GreetingController {
@QueryMapping
public @NonNull String greeting2() {
return "Hello world";
}
}
We also need to add GraphQL schema definition into resources/graphql
directory. For Application1:
type Query {
greeting1: String
}
same for the application 2:
type Query {
greeting2: String
}
The package hierarchy should be something like this for both applications:
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── example
│ └── stitching_sample
│ └── server1
│ ├── BssServer1Application.java
│ └── GreetingController.java
│
└── resources
├── application.properties
└── graphql
└── schema.graphqls
It might be wise to assign different port numbers in your application.properties
or application.yml
using server.port
. We will be using 8081
for application 1 and 8082
for application 2.
Creating the gateway
We will be using lilo inside a GraphQL REST controller but you can use it via a filter or you can use it with different web frameworks as well.
@Controller
public class LiloController {
private static final String SERVER1_NAME = "SERVER1";
private static final String SERVER1_BASE_URL = "http://localhost:8081";
private static final String SERVER2_NAME = "SERVER2";
private static final String SERVER2_BASE_URL = "http://localhost:8082";
private final Lilo lilo;
public LiloController() {
this.lilo =
Lilo.builder()
.addSource(
RemoteSchemaSource.create(
SERVER1_NAME,
new IntrospectionRetrieverImpl(SERVER1_BASE_URL),
new QueryRetrieverImpl(SERVER1_BASE_URL)))
.addSource(
RemoteSchemaSource.create(
SERVER2_NAME,
new IntrospectionRetrieverImpl(SERVER2_BASE_URL),
new QueryRetrieverImpl(SERVER2_BASE_URL)))
.build();
}
@ResponseBody
@PostMapping("/graphql")
public @NonNull Map<String, Object> stitch(@RequestBody final @NonNull GraphQLRequest request) {
return this.lilo.stitch(request.toExecutionInput()).toSpecification();
}
}
When the controller starts, it will create a combined GraphQL schema using the remote sources and it will execute incoming requests using lilo.
We also need additional Retriever definitions. Those classes retrieve the schema or query results. IntrospectionRetriever
retrieves the remote schema and QueryRetriever
is used for every single request for communicating and executing a single query or a mutation on the remote site.
class IntrospectionRetrieverImpl implements SyncIntrospectionRetriever {
private final String schemaUrl;
private final RestTemplate restTemplate;
IntrospectionRetrieverImpl(final @NonNull String schemaUrl) {
this.schemaUrl = schemaUrl + "/graphql";
this.restTemplate =
new RestTemplateBuilder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
@Override
public @NonNull String get(
final @NonNull LiloContext liloContext,
final @NonNull SchemaSource schemaSource,
final @NonNull String query,
final @Nullable Object localContext) {
return Objects.requireNonNull(
this.restTemplate.postForObject(this.schemaUrl, query, String.class));
}
}
and we also need to define our QueryRetriever
class QueryRetrieverImpl implements SyncQueryRetriever {
private final String schemaUrl;
private final RestTemplate restTemplate;
QueryRetrieverImpl(final @NonNull String schemaUrl) {
this.schemaUrl = schemaUrl + "/graphql";
this.restTemplate =
new RestTemplateBuilder() //
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) //
.build(); //
}
@Override
public @NonNull String get(
final @NonNull LiloContext liloContext,
final @NonNull SchemaSource schemaSource,
final @NonNull GraphQLQuery graphQLQuery,
final @Nullable Object localContext) {
return Objects.requireNonNull(
this.restTemplate.postForObject(this.schemaUrl, graphQLQuery.getQuery(), String.class));
}
}
now we are ready we can run all 3 applications and see the results.
curl -X POST \
-H 'content-type: application/json' \
-d '{"query":"{\ngreeting1\ngreeting2\n}","variables":null}' \
http://localhost:8080/graphql
greeting1 will be executed on application1 and greeting2 will be executed on application2 we will get the results in a combined GraphQL response.
You can take a look at the sample implementation code on the official Lilo repo. Check out the sample
Top comments (1)
Thanks for putting this together, we are planning to implement lilo schema stitching in our project but we have a requirement that few microservices expose same graphql schema which is causing the duplicate schema issue with Lilo any workaround or solutions?