DEV Community

Cover image for Simply Order (Part 8) – Querying Orders with Details: GraphQL in Action
Mohamed Hassan
Mohamed Hassan

Posted on

Simply Order (Part 8) – Querying Orders with Details: GraphQL in Action

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:

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-order

Since 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
Enter fullscreen mode Exit fullscreen mode

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 /graphql endpoint 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:

Sequence Diagram

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>
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

By defining this schema, we establish the contract and view for clients. For example, we can hide unnecessary or domain-specific fields. The InventoryItem type exposes an availableQty field while hiding implementation details like stock and reserved in the inventory service.

Also, we expose only the important statuses of OrderStatus, mapping user non-related statuses to meaningful ones (e.g., mapping OPEN & PENDING to just OPEN).

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 @Controller and @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);
}
Enter fullscreen mode Exit fullscreen mode

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;
            });
}
Enter fullscreen mode Exit fullscreen mode

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 } } }"
}
Enter fullscreen mode Exit fullscreen mode

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)