This is the eighth article in our series, where we design a simple order solution for a hypothetical company called Simply Order. The company expects high traffic and needs a resilient, scalable, and distributed order system.
In the previous articles:
- Simply Order (Part 1) Distributed Transactions in Microservices: Why 2PC Doesn’t Fit and How Sagas Help
- Simply Order (Part 2) — Designing and Implementing the Saga Workflow with Temporal
- Simply Order (Part 3) — Linking It All Together: Connecting Services and Watching Temporal in Action
- Simply Order (Part 4) — Reliable Events with the Outbox Pattern (Concepts)
- Simply Order (Part 5) — Hands-On: Building the Outbox Pattern for Reliable Events
- Simply Order (Part 6) – Making APIs Idempotent: Because Users Double-Click and Networks Lie
- Simply Order (Part 7) – Querying Orders with Details: API Composition Pattern
In the previous lesson, we discussed the API composition pattern and how it solves the issue of querying multiple services. In this article, we will implement a new service to act as our API composer using spring-graphql, which runs a GraphQL server exposing data from multiple microservices.
Implementation
The code for this project can be found in this repository:
https://github.com/hassan314159/simply-orderSince this repository is continuously updated, the code specific to this lesson can be found in the add_graphql_api branch. Start with:
git checkout add_graphql_api
We will use spring-graphql to build our service.
The spring-graphql dependency is the official Spring project for building GraphQL APIs in Spring Boot applications. With this dependency, we can define GraphQL schemas, implement resolvers as annotated Java methods, and expose a single
/graphqlendpoint for all queries and mutations. It also includes features like schema introspection and batching (to solve N+1 problems), making it simple to build flexible, type-safe APIs backed by your existing data sources.
Data Flow
Components of GraphQL
In the previous article, we saw that GraphQL consists of these components:
- Language: A query language letting clients request exactly the data they need.
- Schema: Defines available data types, fields, and operations (queries, mutations, subscriptions).
- Server: Processes queries, validates them against the schema, and returns results.
- Resolvers: Functions that fetch data for each field in the schema.
- Data Sources: Databases, APIs, or services where the actual data resides.
The data flow:
Steps to Build
Server: The Execution Engine
The GraphQL server receives queries, validates them against the schema, and orchestrates data fetching through resolvers. Add the spring-graphql dependency:
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql</artifactId>
</dependency>
You can see it here: pom.xml
Schema: The API Contract
The schema defines all available data types, fields, and operations (queries, mutations, subscriptions) your API supports.
Schema components:
-
Types: Define the shape of data (e.g.,
User,Order,Product) -
Fields: Properties of each type (e.g., User has
name,email,id) -
Operations:
-
Query: Fetch data (read-only) -
Mutation: Modify data (create, update, delete) -
Subscription: Real-time updates via WebSocket
-
View the schema here: schema.graphqls
type Query {
order(id: ID!): Order
ordersByCustomer(customerId: ID!, limit: Int = 20): [Order!]!
product(sku: ID!): InventoryItem
}
type Order {
id: ID!
customerId: ID!
createdAt: String!
status: OrderStatus!
items: [OrderItem!]!
totalAmount: Float!
}
enum OrderStatus {
NEW
PROCESSING
SHIPPED
CANCELLED
FAILED
COMPLETED
}
type OrderItem {
sku: ID!
quantity: Int!
price: Float!
inventory: InventoryItem
}
type InventoryItem {
sku: ID!
name: String!
description: String
availableQty: Int!
}
By defining this schema, we establish the contract and view for clients. For example, we can hide unnecessary or domain-specific fields. The
InventoryItemtype exposes anavailableQtyfield while hiding implementation details likestockandreservedin the inventory service.Also, we expose only the important statuses of
OrderStatus, mapping user non-related statuses to meaningful ones (e.g., mappingOPEN&PENDINGto justOPEN).
Java classes mapped to the types are here: model package
Data Sources: The Information Layer
Data sources are where data actually lives—databases, REST APIs, microservices, files, etc.
In our example, the data sources are OrderDB and InventoryDB. We don't call them directly from this microservice; instead, we use HTTP clients to query these services.
Http clients and data models are here: client package
Resolvers: The Data Fetchers
Resolvers fetch or compute data for specific fields in your schema.
We have 2 resolvers in our service:
-
OrderQueryController.java: Query resolvers for root-level queries, annotated with@Controllerand@QueryMapping. -
OrderItemFieldResolver.java: Field resolvers for nested fields, handling the N+1 problem via@BatchMapping.
Resolver code: OrderQueryController.java & OrderItemFieldResolver.java
Query Resolver Example
@QueryMapping
public Mono<Order> order(@Argument String id) {
return orderClient.getOrder(id)
.switchIfEmpty(Mono.error(new RuntimeException("Order not found")))
.map(orderMapper::toModel);
}
Batch Resolver: Solving the N+1 Problem
To resolve nested fields like inventory on OrderItem, instead of querying the inventory service for each item (N queries), we use batching to fetch all required SKUs in one query:
@BatchMapping(typeName = "OrderItem", field = "inventory")
public Mono<Map<OrderItem, InventoryItem>> inventory(List<OrderItem> items) {
List<String> skus = items.stream()
.map(OrderItem::sku)
.distinct()
.toList();
return inventoryClient.getBySkus(skus)
.map(inventoryDtos -> {
Map<String, InventoryItem> bySku = inventoryDtos.stream()
.map(inventoryMapper::toModel)
.collect(Collectors.toMap(InventoryItem::sku, inv -> inv));
Map<OrderItem, InventoryItem> result = new HashMap<>();
for (OrderItem item : items) {
result.put(item, bySku.get(item.sku()));
}
return result;
});
}
Language: Query What You Need
GraphQL query language allows clients to specify exactly the data they want.
Sample query to fetch an order by id with its details:
{
"query": "query { order(id: "ORD-1001") { id customerId createdAt status totalAmount items { sku quantity unitPrice } } }"
}
The outer query key contains the GraphQL operation string. Inside, the nested query specifies the fetch operation. Curly braces specify which fields to retrieve.
Homework
We have implemented the fetch operations needed for our business. However, currently, we must call the Order service directly to create orders. Can you create a mutation operation in our API service to provide a single entry point for all client interactions?
Wrap-up
In this article, the GraphQL API service was introduced as an effective API composition layer for the Simply Order system. Using spring-graphql, we defined a schema that exposes consolidated views from multiple microservices, hiding internal complexities and mapping data meaningfully for clients. We implemented query resolvers with efficient batch loading to solve common performance problems like the N+1 query issue. This design enables a scalable, flexible, and type-safe API interface that fits the needs of a high-traffic distributed order system.

Top comments (0)