Combining multiple services into one unified API

Schema stitching with Spring Boot 3.0 and GraphQL

Anurag Pandit
Globant
11 min readSep 26, 2024

--

Managing data across multiple small services can be challenging, but GraphQL schema stitching offers a solution by combining these services into a single API. This approach simplifies data retrieval, allowing you to get all the information you need with just one request.

In this article, we’ll dive into how GraphQL schema stitching can integrate microservices into a unified API. Whether you’re new to GraphQL or have some experience, this guide will help you understand the process of merging your services seamlessly.

We’ll explore schema stitching in detail, explaining why it’s a valuable tool for code reuse and data integration. Through practical examples and clear explanations, you’ll learn how to unify multiple services into a single API, enhancing your GraphQL setup. Additionally, we’ll cover how schema stitching makes data integration more straightforward, improving both efficiency and the development process. By the end of this guide, you’ll see how schema stitching can help you build robust and scalable GraphQL APIs with ease.

Understanding Schema Stitching

In this section, we’ll explain how GraphQL schema stitching integrates multiple services into a unified API, detailing the stitching process, data handling, and key tools like the Introspection Retriever and Query Retriever for seamless data integration.

GraphQL schema stitching is a game-changing technique for integrating multiple GraphQL services into one unified API. Think of it like assembling a puzzle — each piece is a different GraphQL schema, and schema stitching connects these pieces to create a complete picture.

Here’s how it works: when you start the stitching process, the schema stitching library collects schemas from various sources. These could be external services or different parts of your project. After gathering the schemas, the library maps queries and mutations to their respective sources, much like sorting mail based on its destination.

As new requests come in, the stitching library takes over. It examines each request, directs it to the correct source for data retrieval, and then combines the results into a single, comprehensive response. This orchestration is managed by the controller, which, when initialized, uses tools like Lilo to merge schemas from different sources into a unified schema.

Two critical components make this process work smoothly: the Introspection Retriever, which pulls schema definitions from other sources, and the Query Retriever, which executes queries and retrieves data from the appropriate places. Together, these components ensure that schema stitching is effective, making data integration straightforward and efficient.

Benefits of Schema Stitching

Here, we’ll see the benefits of GraphQL schema stitching, including its ability to unify multiple schemas into a single endpoint, simplify microservice and third-party API integration, support schema evolution, and optimize performance. We’ll also show how schema stitching enhances data management and improves experiences for both developers and users.

Schema stitching brings impressive advantages, especially when dealing with microservices and multiple data sources. In a typical microservices setup, each service might have its own GraphQL API. Schema stitching simplifies integration by merging these separate schemas into a single, unified API. This means clients can access all necessary data through one endpoint, making data access smoother and more efficient.

When it comes to integrating third-party APIs that use GraphQL, schema stitching is incredibly useful. It seamlessly combines external schemas with your own, creating a unified experience for clients who need data from various sources. This is especially valuable when dealing with data spread across different databases or services, as schema stitching consolidates this dispersed information into one GraphQL endpoint.

As applications grow and evolve, so do their GraphQL schemas. Schema stitching makes it easy to add new features and update schemas without disrupting existing clients. It’s like fitting new puzzle pieces into an evolving picture, ensuring everything works together harmoniously.

Another key benefit is performance optimization. Schema stitching improves data retrieval efficiency by distributing the workload across various sources based on client queries. This approach reduces the load on the main server and speeds up response times, much like delegating tasks to different team members to keep things running smoothly.

Overall, schema stitching simplifies data integration, enhances system performance, and boosts productivity. It makes managing GraphQL environments easier and improves the experience for both developers and users.

Practical Implementation of Schema stitching

In this section, we’ll explore schema stitching with a practical example using two GraphQL services: Employee and Department. We’ll outline their schemas and show how a data gateway service, enhanced by the Lilo library, merges these into a unified API. We’ll also cover key components like the Introspection Retriever and Query Retriever, demonstrating how they simplify and improve data integration.

Employee Service

Setting up a GraphQL service to manage employee details is straightforward yet powerful. The Employee Service provides all the information about employees, such as their employee IDs, first names, and last names. Here’s a basic idea of what the Employee schema might look like:

type Employee {
employeeID: ID!
firstName: String!
lastName: String!
}

type Query {
findAllEmployees: [Employee]!
findByEmployeeID(employeeID: ID!): Employee
}

type Mutation {
newEmployee(firstName: String!, lastName: String!) : Employee!
}

This schema defines the Employee type with fields for employeeId, firstName, and lastName. It also defines queries to fetch a list of employees or a specific employee by ID.

Department Service

Similar to the Employee Service, the Department Service manages details about departments, including department IDs, names, and lists of departments. Here’s a basic idea of what the Department schema might look like:

type Department {
departmentID: ID!
departmentName: String!
departmentNumber: String!
}

type Query {
findAllDepartments: [Department]!
findByDepartmentID(departmentID: ID!): Department
}

type Mutation {
newDepartment(departmentName: String!, departmentNumber: String!) : Department!
}

The Department type includes fields for departmentId, departmentName, and departmentNumber where departmentis an array of Department objects.

The Query type defines operations to fetch a list of departments or a specific department by ID.

DataGateway: Integrating Services with Schema Stitching

In this section, we’ll illustrate how schema stitching connects Employee and Department services into a unified API using the Lilo library. We’ll explain how the data gateway service fetches and maps schemas, handles incoming queries and consolidates responses. We’ll also highlight the roles of the Introspection Retriever and Query Retriever in ensuring efficient data integration and management.

To seamlessly connect our Employee and Department services, we use a data gateway service enhanced by schema stitching through the Lilo library. This setup simplifies integrating these services into a single, unified API.

When we set up the data gateway, it starts by fetching schemas from both the Employee and Department services. These schemas might come from different sources, whether remote or internal. The Lilo library then takes these schemas and maps the GraphQL queries and mutations to their respective services, effectively organizing the data flow.

Once a request is received, the data gateway service takes charge. It analyzes the incoming query to determine which service — Employee or Department — should handle it. After directing the query to the appropriate service, it consolidates the responses into a single, comprehensive answer. This approach ensures that clients receive all the data they need in one unified response.

The controller plays a crucial role at startup. It initializes by merging the schemas from the various sources using Lilo. This unified schema helps streamline data retrieval and response handling, making the entire process more efficient.

Two key components make this possible: the Introspection Retriever, which gathers schema definitions from remote GraphQL services, and the Query Retriever, which handles queries asynchronously. The Query Retriever fetches data from the appropriate sources based on the client’s request, ensuring a smooth and responsive data integration experience.

This approach to integrating services through schema stitching with Lilo not only simplifies data management but also enhances the efficiency of accessing and handling data from multiple sources.

Simplifying Data Integration

The diagram provides an overview of how schema stitching is implemented using the LILO library to integrate Employee and Department services. It visually represents the consolidation and unification of these services into a single cohesive system.

Schema stitching

Instead of dealing with multiple endpoints, clients could get all the data they needed with just one request. It was a game-changer in terms of efficiency and simplicity.

Here’s a snippet of how we might configure the data gateway using Lilo:

Pom.xml

<dependency>
<groupId>io.fria</groupId>
<artifactId>lilo</artifactId>
<version>22.11.0</version>
</dependency>

Enhancing Query Efficiency with QueryRetrieverImpl

Here, we’ll explore the QueryRetrieverImpl class in our GraphQL schema stitching setup. We’ll explain how it facilitates efficient data fetching by setting up connections to remote GraphQL endpoints, handling asynchronous requests, and improving system responsiveness and scalability.

package com.example.DataGateway.Controller;

import io.fria.lilo.AsyncQueryRetriever;
import io.fria.lilo.GraphQLQuery;
import io.fria.lilo.LiloContext;
import io.fria.lilo.SchemaSource;
import org.jetbrains.annotations.Nullable;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.client.RestTemplate;

import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;

public class QueryRetrieverImpl implements AsyncQueryRetriever {

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 CompletableFuture<String> get(
@NonNull LiloContext liloContext,
@NonNull SchemaSource schemaSource,
@NonNull GraphQLQuery graphQLQuery,
@Nullable Object localContext) {

String query = Objects.requireNonNull(graphQLQuery.getQuery(),
"GraphQL query cannot be null");

return CompletableFuture.supplyAsync(() -> {
try {
return Objects.requireNonNull(
restTemplate.postForObject(schemaUrl,
query, String.class),
"Response from server cannot be null"
);
} catch (RuntimeException e) {
throw new CompletionException(e);
}
});
}

}

In our GraphQL schema stitching setup, the QueryRetrieverImpl class is vital for efficient data fetching. Here’s a straightforward breakdown of how it works:

When we initialize QueryRetrieverImpl, it sets up a connection to remote GraphQL endpoints by configuring the schema URL and using a RestTemplate. This setup ensures that we can communicate seamlessly with external services.

The core of QueryRetrieverImpl its get method, which handles the asynchronous fetching of data. This method takes a GraphQL query, sends it to the specified remote schema URL, and initiates an asynchronous request. It returns a CompletableFuture, allowing the system to process responses without blocking other operations. Additionally, the method is equipped with error handling to deal with any issues that might arise during query execution.

Leveraging asynchronous data fetching QueryRetrieverImpl significantly enhances the responsiveness and scalability of our setup. It processes GraphQL queries concurrently, which improves performance and ensures a smoother user experience.

Streamlining Schema Retrieval with IntrospectionRetrieverImpl

In this section, we’ll explore the role of the IntrospectionRetrieverImpl class in GraphQL schema stitching. We’ll discuss how it connects to remote GraphQL endpoints, retrieves schema definitions synchronously, and ensures the validity of these schemas. We’ll highlight how IntrospectionRetrieverImpl facilitates the efficient and reliable integration of multiple GraphQL schemas, making it a key component in our schema stitching process.

package com.example.DataGateway.Controller;

import io.fria.lilo.LiloContext;
import io.fria.lilo.SchemaSource;
import io.fria.lilo.SyncIntrospectionRetriever;
import org.jetbrains.annotations.Nullable;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.client.RestTemplate;

import java.util.Objects;

public 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));
}
}

In our GraphQL schema stitching process, the IntrospectionRetrieverImpl class plays a crucial role in managing schema definitions. Here’s a clear overview of its function:

When we create an instance of IntrospectionRetrieverImpl, it sets up the necessary configuration to connect with remote GraphQL endpoints. This involves setting the schema URL and initializing a RestTemplate for smooth communication with external services.

The core functionality of IntrospectionRetrieverImpl its get method, which retrieves schema definitions synchronously. This method sends a GraphQL introspection query to the specified schema URL to obtain the schema definition. Once the response is received, it checks to ensure that the schema definition is valid and then returns it as a string.

Handling schema retrieval in this manner, IntrospectionRetrieverImpl ensures that integrating multiple GraphQL schemas is both efficient and reliable. This makes it an essential component of our schema stitching setup, ensuring that the integration process is smooth and seamless.

Simplifying Schema Stitching with LiloController in Spring Boot

In this section, we’ll explain the role of the LiloController class in simplifying schema stitching within a GraphQL setup using Spring Boot and the Lilo library. We’ll cover how the LiloController integrates multiple GraphQL schemas, manages configurations, and handles requests efficiently. Additionally, we’ll highlight how this setup enhances GraphQL API performance and streamlines development.

package com.example.DataGateway.Controller;

import io.fria.lilo.GraphQLRequest;
import io.fria.lilo.Lilo;
import io.fria.lilo.RemoteSchemaSource;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;

@RestController
public class LiloController {

private static final String EMPLOYEE = "EMPLOYEE";
private static final String EMPLOYEE_BASE_URL = "http://localhost:8081";
private static final String DEPARTMENT = "DEPARTMENT";
private static final String DEPARTMENT_BASE_URL = "http://localhost:8082";

private final Lilo lilo;

public LiloController() {
this.lilo =
Lilo.builder()
.addSource(
RemoteSchemaSource.create(
EMPLOYEE,
new IntrospectionRetrieverImpl(EMPLOYEE_BASE_URL),
new QueryRetrieverImpl(EMPLOYEE_BASE_URL)))
.addSource(
RemoteSchemaSource.create(
DEPARTMENT,
new IntrospectionRetrieverImpl(DEPARTMENT_BASE_URL),
new QueryRetrieverImpl(DEPARTMENT_BASE_URL)))
.build();
}

@PostMapping("/graphql")
public @NonNull Map<String, Object> stitch(@RequestBody
final @NonNull GraphQLRequest request) {
Map<String, Object> result = this.lilo
.stitch(request.toExecutionInput()).toSpecification();
return result;
}
}

In our GraphQL setup, the LiloController class is key to simplifying schema stitching with Spring Boot and the Lilo library. Here’s how it works:

The LiloController is designed to streamline the integration of multiple GraphQL schemas into a single API. It starts by defining constants for the service names and base URLs of the Employee and Department services, which helps keep the configuration organized and clear.

Upon initialization, the controller creates an Lilo instance that integrates schemas from both the Employee and Department services. This is accomplished using IntrospectionRetrieverImpl and QueryRetrieverImpl, which handles schema retrieval and query execution, respectively. By consolidating these schemas, Lilo it enables a unified GraphQL API that combines multiple data sources seamlessly.

The core functionality of the LiloController method is found in it. This method processes POST requests directed at the /graphql endpoint. It uses the configured Lilo instance to merge schemas according to the provided GraphQLRequest. The result is a comprehensive dataset returned in response to a single request, showcasing efficient data integration.

This approach not only simplifies the schema stitching process but also enhances the performance of GraphQL APIs and streamlines development. By leveraging LiloController, we can manage complex data integrations more effectively, ultimately benefiting end users with a smoother experience.

Result:

Here is the result when we make an API request to retrieve data from both services.

Conclusion: Simplifying Data Fetching with GraphQL Schema Stitching

In conclusion, GraphQL schema stitching has fundamentally transformed how we handle data fetching, significantly boosting speed and efficiency. By merging multiple services into a single endpoint, we’ve simplified the process for clients to access the needed data, improving productivity.

In my experience, schema stitching has been a game-changer. It has made development more straightforward and allowed us to complete projects faster. It has also streamlined the integration of various data sources, making the process quicker and easier.

Furthermore, GraphQL schema stitching has opened up new opportunities for innovation in data integration. It has empowered developers to build robust, scalable applications with greater ease. As we continue to explore innovative solutions, schema stitching remains a central strategy for achieving seamless data access and delivering exceptional user experiences.

In essence, GraphQL schema stitching is an invaluable tool that offers real, tangible benefits. It has played a crucial role in driving successful project outcomes, and we are excited to continue leveraging its potential to explore new possibilities and provide outstanding user experiences.

--

--

Anurag Pandit
Globant

Java developer skilled in Spring Boot, REST, GraphQL, and microservices, focused on building scalable applications and sharing backend insights.