Spring GraphQL
REST has been the default choice for building APIs for a long time.
For many applications, it works extremely well. You expose endpoints such as:
GET /api/customers/101
GET /api/customers/101/orders
POST /api/orders
PUT /api/customers/101As applications grow, however, clients often need different combinations of data.
A mobile application might need only:
customerId
namewhile a web application might need:
customerId
name
email
orders
orderItemsWith REST, this can lead to multiple endpoints, additional requests, or responses containing more data than the client actually needs.
GraphQL takes a different approach.
Instead of exposing many resource-oriented endpoints, you expose a typed API schema and allow the client to specify the data it wants.
Spring for GraphQL brings this programming model into the Spring ecosystem.
In this article, we’ll start with the GraphQL fundamentals, build a Spring Boot application, connect it to a database, create queries and mutations, understand resolvers, nested objects, arguments, variables, error handling, pagination, DataLoader, N+1 problems, security, testing, and finally look at how GraphQL fits into a production architecture.
1. What is GraphQL?
GraphQL is an API query language and runtime.
The key idea is simple:
The client describes the shape of the data it wants.
Suppose our application contains customers and orders.
A REST API might expose:
GET /customers/101and return:
{
"id": 101,
"name": "Manish",
"email": "manish@example.com",
"phone": "9999999999",
"address": "Pune"
}But perhaps the client only needs:
id
nameGraphQL allows the client to ask for exactly that.
query {
customer(id: 101) {
id
name
}
}The response mirrors the query:
{
"data": {
"customer": {
"id": 101,
"name": "Manish"
}
}
}If the client needs the email as well:
query {
customer(id: 101) {
id
name
email
}
}The server can return:
{
"data": {
"customer": {
"id": 101,
"name": "Manish",
"email": "manish@example.com"
}
}
}The client controls the requested response shape.
2. GraphQL vs REST
Suppose we have:
Customer
|
+--- OrdersIn REST, you might have:
GET /customers/101
GET /customers/101/ordersThe frontend might need two requests.
With GraphQL:
query {
customer(id: 101) {
id
name orders {
id
amount
status
}
}
}One GraphQL operation can request the related data.
This is one of the reasons GraphQL can be useful for applications where clients have different data requirements.
3. GraphQL has a strongly typed schema
A GraphQL API is described using a schema.
For example:
type Customer {
id: ID!
name: String!
email: String!
}This says:
Customeris an object type.idis anID.nameis aString.emailis aString.!means the field cannot be null.
The schema becomes the contract between the client and server.
4. GraphQL’s main building blocks
There are several concepts you should understand before writing Spring code.
Schema
|
+-- Query
|
+-- Mutation
|
+-- Subscription
|
+-- Types
|
+-- Fields
|
+-- Arguments
|
+-- Input Types
|
+-- Interfaces
|
+-- Unions
|
+-- EnumsThe most commonly used ones are:
Query
Used to retrieve data.
query {
customer(id: 101) {
name
}
}Mutation
Used to change data.
mutation {
createCustomer(
name: "Manish"
email: "manish@example.com"
) {
id
name
}
}Subscription
Used for real-time updates.
For example:
subscription {
paymentStatusChanged(paymentId: "TX100") {
status
}
}Subscriptions are useful for things such as live payment status, notifications, dashboards, and real-time monitoring.
5. Understanding the GraphQL schema
Let’s define our application.
We’ll build a small Customer and Order Management API.
Our domain looks like:
Customer
|
+---- Order
|
+---- Order
|
+---- OrderA customer has:
id
name
emailAn order has:
id
amount
statusOur schema can be:
type Customer {
id: ID!
name: String!
email: String!
orders: [Order!]!
}type Order {
id: ID!
amount: Float!
status: OrderStatus!
}enum OrderStatus {
CREATED
PAID
CANCELLED
}
6. Query
Now define what clients can request.
type Query {customer(id: ID!): Customer customers: [Customer!]! orders: [Order!]!
}
This means the API supports:
customer(id: ID!)and:
customersand:
orders7. Mutation
Now let’s allow clients to create customers.
type Mutation {createCustomer(
name: String!
email: String!
): Customer!}
The client can call:
mutation {
createCustomer(
name: "Manish"
email: "manish@example.com"
) {
id
name
email
}
}8. Setting up Spring Boot
Let’s build the project.
We’ll use:
Java 17+
Spring Boot
Spring for GraphQL
Spring Data JPA
PostgreSQL
MavenA basic project structure:
graphql-demo
|
+-- src/main/java/com/example/graphql
| |
| +-- GraphqlDemoApplication.java
| |
| +-- customer
| | |
| | +-- Customer.java
| | +-- CustomerRepository.java
| | +-- CustomerController.java
| |
| +-- order
| |
| +-- Order.java
| +-- OrderRepository.java
|
+-- src/main/resources
|
+-- application.properties
|
+-- graphql
|
+-- schema.graphqls9. Maven dependencies
For Spring Boot, add:
<dependencies><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> <dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency> <dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency> <dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency></dependencies>
The exact dependency versions should come from the Spring Boot version you’re using rather than copying arbitrary versions from an old tutorial.
10. Application configuration
For PostgreSQL:
spring.datasource.url=jdbc:postgresql://localhost:5432/graphql_demo
spring.datasource.username=postgres
spring.datasource.password=postgresspring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=truespring.graphql.graphiql.enabled=true
The last property enables GraphiQL, a browser-based interface useful for exploring and testing the GraphQL API.
11. Creating the database
Start PostgreSQL and create:
CREATE DATABASE graphql_demo;You can also run PostgreSQL using Docker.
services: postgres:
image: postgres:16
container_name: graphql-postgres
environment:
POSTGRES_DB: graphql_demo
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"Start it:
docker compose up -d12. Creating the Customer entity
@Entity
@Table(name = "customers")
public class Customer {@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private String name; private String email; // constructors public Customer() {
} public Customer(String name, String email) {
this.name = name;
this.email = email;
} // getters and setters
}
13. Creating the Order entity
@Entity
@Table(name = "orders")
public class Order {@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Double amount; @Enumerated(EnumType.STRING)
private OrderStatus status; @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer; public Order() {
} // getters and setters
}
And:
public enum OrderStatus { CREATED,
PAID,
CANCELLED
}14. Repositories
Customer repository:
public interface CustomerRepository
extends JpaRepository<Customer, Long> {
}Order repository:
public interface OrderRepository
extends JpaRepository<Order, Long> { List<Order> findByCustomerId(Long customerId);
}Now we have our persistence layer.
15. Creating the GraphQL schema
Create:
src/main/resources/graphql/schema.graphqlsPut this inside:
type Customer {
id: ID!
name: String!
email: String!
orders: [Order!]!
}type Order {
id: ID!
amount: Float!
status: OrderStatus!
}enum OrderStatus {
CREATED
PAID
CANCELLED
}type Query { customer(id: ID!): Customer customers: [Customer!]! orders: [Order!]!
}type Mutation { createCustomer(
name: String!
email: String!
): Customer!
}
This schema is now the contract for our API.
16. How Spring connects Java to GraphQL
This is one of the most important Spring GraphQL concepts.
In a traditional Spring MVC application you might write:
@GetMapping("/customers/{id}")GraphQL doesn’t work primarily through one controller method per URL.
Instead, Spring GraphQL maps GraphQL fields to Java methods.
For example:
@QueryMapping
public Customer customer(Long id) {
return customerService.findById(id);
}The @QueryMapping tells Spring:
This Java method handles the GraphQL customer query.
17. Customer GraphQL controller
Create:
@Controller
public class CustomerController {private final CustomerRepository customerRepository; public CustomerController(
CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
} @QueryMapping
public Customer customer(@Argument Long id) { return customerRepository
.findById(id)
.orElse(null);
} @QueryMapping
public List<Customer> customers() { return customerRepository.findAll();
}
}
Notice that this is:
@Controllerrather than:
@RestControllerbecause we’re dealing with GraphQL mappings rather than conventional REST endpoint mappings.
18. Calling the query
Start the application.
Open GraphiQL.
You can execute:
query {
customer(id: 1) {
id
name
email
}
}The response might be:
{
"data": {
"customer": {
"id": "1",
"name": "Manish",
"email": "manish@example.com"
}
}
}Notice that the response structure follows the query structure.
19. Requesting only selected fields
Suppose the client asks:
query {
customer(id: 1) {
id
name
}
}The response is:
{
"data": {
"customer": {
"id": "1",
"name": "Manish"
}
}
}The client didn’t request email, so it doesn’t appear in the response.
This is one of the biggest differences from a traditional REST endpoint returning a fixed DTO.
20. Querying multiple customers
Our schema contains:
customers: [Customer!]!and our Java method:
@QueryMapping
public List<Customer> customers() {
return customerRepository.findAll();
}A client can request:
query {
customers {
id
name
email
}
}Response:
{
"data": {
"customers": [
{
"id": "1",
"name": "Manish",
"email": "manish@example.com"
},
{
"id": "2",
"name": "Rahul",
"email": "rahul@example.com"
}
]
}
}21. GraphQL arguments
Consider:
customer(id: ID!): CustomerThe id is an argument.
Spring maps it using:
@Argument Long idExample:
@QueryMapping
public Customer customer(@Argument Long id) { return customerRepository
.findById(id)
.orElse(null);
}GraphQL:
query {
customer(id: 1) {
id
name
}
}22. Mutations
Queries read data.
Mutations change data.
Add this to your controller:
@MutationMapping
public Customer createCustomer(
@Argument String name,
@Argument String email) {Customer customer =
new Customer(name, email); return customerRepository.save(customer);
}
Now execute:
mutation {
createCustomer(
name: "John"
email: "john@example.com"
) {
id
name
email
}
}GraphQL might return:
{
"data": {
"createCustomer": {
"id": "3",
"name": "John",
"email": "john@example.com"
}
}
}23. Why GraphQL mutations are different from REST POST
REST usually communicates intent through HTTP methods:
GET
POST
PUT
PATCH
DELETEGraphQL uses operations such as:
query
mutation
subscriptionThe transport is commonly HTTP, but GraphQL itself defines the operation semantics.
For example:
mutation {
createCustomer(...)
}is a GraphQL mutation even though the request itself is typically sent through HTTP.
24. Input types
Passing many arguments individually can become awkward.
Instead of:
createCustomer(
name: String!
email: String!
phone: String!
address: String!
)we can define an input type:
input CreateCustomerInput {
name: String!
email: String!
phone: String
address: String
}Then:
type Mutation { createCustomer(
input: CreateCustomerInput!
): Customer!
}The client can send:
mutation {
createCustomer(
input: {
name: "Manish"
email: "manish@example.com"
phone: "9999999999"
address: "Pune"
}
) {
id
name
email
}
}Input types become especially useful as mutations become more complex.
25. Variables
Hardcoding values directly into a query isn’t ideal.
Instead of:
mutation {
createCustomer(
input: {
name: "Manish"
email: "manish@example.com"
}
) {
id
name
}
}define variables:
mutation CreateCustomer($input: CreateCustomerInput!) { createCustomer(input: $input) {
id
name
email
}
}Variables:
{
"input": {
"name": "Manish",
"email": "manish@example.com"
}
}This is much more suitable for real frontend applications.
26. Nested fields
Now let’s request orders along with the customer.
Our schema says:
type Customer {id: ID!
name: String!
email: String! orders: [Order!]!
}
A query can therefore be:
query {
customer(id: 1) {id
name orders {
id
amount
status
}
}
}
This is where GraphQL becomes particularly interesting.
The Customer query returns the customer, but GraphQL also needs to figure out how to resolve:
Customer.orders27. Field mappings
Spring GraphQL provides @SchemaMapping for resolving fields that aren’t simply direct properties.
For example:
@SchemaMapping
public List<Order> orders(Customer customer) { return orderRepository
.findByCustomerId(customer.getId());
}This tells Spring:
When GraphQL asks for the orders field of a Customer, use this method.
Now the query:
query {
customer(id: 1) {
id
name orders {
id
amount
status
}
}
}can resolve correctly.
28. Understanding the resolver flow
The request looks like:
GraphQL Query
|
↓
customer(id: 1)
|
↓
@QueryMapping
|
↓
Customer
|
↓
orders
|
↓
@SchemaMapping
|
↓
OrderRepository
|
↓
OrdersThe GraphQL execution engine walks through the requested fields.
That’s an important mental model.
29. The N+1 problem
Now we hit a classic GraphQL performance problem.
Suppose the client requests:
query {
customers {
id
name orders {
id
amount
}
}
}Suppose there are 100 customers.
Your application could execute:
1 query → fetch 100 customers100 queries → fetch orders for each customerTotal:
101 database queriesThis is called the N+1 problem.
It’s not unique to GraphQL, but nested GraphQL fields make it very easy to create.
30. DataLoader
One common solution is batching.
Instead of:
Customer 1 → query orders
Customer 2 → query orders
Customer 3 → query orders
...
Customer 100 → query ordersyou can batch the IDs:
[1, 2, 3, ..., 100]and execute one query:
SELECT *
FROM orders
WHERE customer_id IN (...)Conceptually:
GraphQL
↓
100 customers
↓
DataLoader
↓
batch IDs
↓
one database query
↓
group results
↓
return ordersSpring for GraphQL provides integration with DataLoader patterns for this purpose.
This becomes important when you have deeply nested data.
31. Pagination
Returning thousands of records from:
customersis usually not a good idea.
GraphQL APIs often use cursor-based pagination.
For example:
type CustomerConnection {
edges: [CustomerEdge!]!
pageInfo: PageInfo!
}type CustomerEdge {
cursor: String!
node: Customer!
}type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
The query might look like:
query {
customers(first: 20) {edges {
cursor node {
id
name
}
} pageInfo {
hasNextPage
endCursor
}
}
}
This approach scales much better than returning an unbounded list.
32. Error handling
Suppose the client asks:
query {
customer(id: 999999) {
id
name
}
}There may not be a customer with that ID.
GraphQL responses have a standard structure that can contain both:
data
errorsFor example:
{
"data": {
"customer": null
},
"errors": [
{
"message": "Customer not found"
}
]
}This differs somewhat from the traditional REST approach where you might simply return:
404 Not FoundGraphQL has its own error propagation model.
33. Nullability matters
Consider:
name: String!The ! means:
name cannot be nullCompare:
name: Stringwith:
name: String!The first allows:
"name": nullThe second does not.
The same concept applies to lists.
[Customer]and:
[Customer!]!have different nullability semantics.
Understanding nullability is important when designing a stable GraphQL schema.
34. Interfaces
GraphQL supports interfaces.
Suppose we have:
Payment
|
+-- CardPayment
|
+-- UPIPaymentWe could define:
interface Payment {
id: ID!
amount: Float!
}Then:
type CardPayment implements Payment {
id: ID!
amount: Float!
cardLast4: String!
}type UpiPayment implements Payment {
id: ID!
amount: Float!
upiId: String!
}Clients can query:
query {
payments {
id
amount
}
}and request type-specific fields using fragments.
35. Fragments
Suppose:
type CardPayment implements Payment {
id: ID!
amount: Float!
cardLast4: String!
}You can write:
query {
payments {id
amount ... on CardPayment {
cardLast4
} }
}
Fragments allow reusable selections of fields.
Get Manish Pandey’s stories in your inbox
Join Medium for free to get updates from this writer.
For example:
fragment CustomerDetails on Customer {
id
name
email
}Then:
query {
customer(id: 1) {
...CustomerDetails
}
}36. Enums
We already used:
enum OrderStatus {
CREATED
PAID
CANCELLED
}Enums restrict a field to known values.
That is much safer than:
status: Stringwhere a client could send arbitrary strings such as:
"paid"
"PAID"
"Payed"
"completed"A strongly typed enum reduces this class of problem.
37. GraphQL and Spring Security
GraphQL does not remove the need for authentication and authorization.
Suppose:
User Arequests:
customer(id: 100)You may need to check whether that user is allowed to access customer 100.
Spring Security can still sit in front of the GraphQL endpoint.
A typical architecture is:
Client
↓
Authentication
↓
Spring Security
↓
GraphQL
↓
Authorization
↓
Service
↓
DatabaseAuthentication answers:
Who is this user?
Authorization answers:
Is this user allowed to access this data?
Those are separate concerns.
38. Field-level authorization
GraphQL introduces another interesting problem.
Suppose:
type Customer { id: ID!
name: String!
email: String!
internalRiskScore: Int
}A normal customer might be allowed to request:
id
name
emailbut not:
internalRiskScoreThis means authorization sometimes needs to be considered at the field level rather than only at the endpoint level.
GraphQL has one endpoint, but potentially many fields with different access requirements.
This is one of the biggest architectural differences compared with traditional REST APIs.
39. GraphQL endpoint
A Spring GraphQL application commonly exposes a GraphQL endpoint such as:
/graphqlThe client sends GraphQL operations to that endpoint.
For example:
POST /graphqlwith:
{
"query": "query { customers { id name } }"
}Unlike REST, you don’t normally create:
/customers
/customers/{id}
/customers/{id}/ordersfor every data shape.
The schema describes what can be queried.
40. GraphiQL
For development, GraphiQL is extremely useful.
You can write:
query {
customers {
id
name
email
}
}and immediately see the response.
It also lets you explore the schema and understand available fields.
For developers working on GraphQL APIs, schema exploration becomes an important part of the development workflow.
41. Complete project structure
Let’s bring everything together.
graphql-demo
│
├── pom.xml
│
└── src
└── main
├── java
│ └── com.example.graphql
│ │
│ ├── GraphqlDemoApplication.java
│ │
│ ├── customer
│ │ ├── Customer.java
│ │ ├── CustomerRepository.java
│ │ └── CustomerController.java
│ │
│ └── order
│ ├── Order.java
│ ├── OrderStatus.java
│ └── OrderRepository.java
│
└── resources
│
├── application.properties
│
└── graphql
└── schema.graphqls42. Main application
package com.example.graphql;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class GraphqlDemoApplication { public static void main(String[] args) { SpringApplication.run(
GraphqlDemoApplication.class,
args
);
}
}
43. Customer entity
package com.example.graphql.customer;import jakarta.persistence.*;@Entity
@Table(name = "customers")
public class Customer { @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private String name; private String email; public Customer() {
} public Customer(String name, String email) {
this.name = name;
this.email = email;
} public Long getId() {
return id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public String getEmail() {
return email;
} public void setEmail(String email) {
this.email = email;
}
}
44. Order entity
package com.example.graphql.order;import com.example.graphql.customer.Customer;
import jakarta.persistence.*;@Entity
@Table(name = "orders")
public class Order { @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Double amount; @Enumerated(EnumType.STRING)
private OrderStatus status; @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer; public Order() {
} public Long getId() {
return id;
} public Double getAmount() {
return amount;
} public OrderStatus getStatus() {
return status;
} public Customer getCustomer() {
return customer;
} public void setAmount(Double amount) {
this.amount = amount;
} public void setStatus(OrderStatus status) {
this.status = status;
} public void setCustomer(Customer customer) {
this.customer = customer;
}
}
45. Repositories
package com.example.graphql.customer;import org.springframework.data.jpa.repository.JpaRepository;public interface CustomerRepository
extends JpaRepository<Customer, Long> {
}
And:
package com.example.graphql.order;import org.springframework.data.jpa.repository.JpaRepository;import java.util.List;public interface OrderRepository
extends JpaRepository<Order, Long> { List<Order> findByCustomerId(Long customerId);
}
46. GraphQL controller
package com.example.graphql.customer;import com.example.graphql.order.Order;
import com.example.graphql.order.OrderRepository;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;import java.util.List;@Controller
public class CustomerController { private final CustomerRepository customerRepository; private final OrderRepository orderRepository; public CustomerController(
CustomerRepository customerRepository,
OrderRepository orderRepository) { this.customerRepository = customerRepository;
this.orderRepository = orderRepository;
} @QueryMapping
public Customer customer(
@Argument Long id) { return customerRepository
.findById(id)
.orElse(null);
} @QueryMapping
public List<Customer> customers() { return customerRepository.findAll();
} @MutationMapping
public Customer createCustomer(
@Argument String name,
@Argument String email) { Customer customer =
new Customer(name, email); return customerRepository.save(customer);
} @SchemaMapping
public List<Order> orders(Customer customer) { return orderRepository
.findByCustomerId(customer.getId());
}
}
This small controller now handles:
Query:
customer
customersMutation:
createCustomerNested field:
Customer.orders
47. Schema
type Customer {
id: ID!
name: String!
email: String!
orders: [Order!]!
}type Order {
id: ID!
amount: Float!
status: OrderStatus!
}enum OrderStatus {
CREATED
PAID
CANCELLED
}type Query { customer(id: ID!): Customer customers: [Customer!]! orders: [Order!]!
}type Mutation { createCustomer(
name: String!
email: String!
): Customer!
}
48. Running the application
Start PostgreSQL:
docker compose up -dThen run:
./mvnw spring-boot:runOnce Spring Boot starts, open your GraphiQL endpoint.
Depending on your Spring configuration/version, the GraphiQL URL may be:
/graphiqlThe GraphQL endpoint itself is:
/graphql49. Test the first query
Run:
query {
customers {
id
name
email
}
}If customers exist, you’ll get:
{
"data": {
"customers": [
{
"id": "1",
"name": "Manish",
"email": "manish@example.com"
}
]
}
}50. Create a customer
Run:
mutation {
createCustomer(
name: "Rahul"
email: "rahul@example.com"
) {
id
name
email
}
}The response could be:
{
"data": {
"createCustomer": {
"id": "2",
"name": "Rahul",
"email": "rahul@example.com"
}
}
}51. Query nested orders
Suppose customer 1 has three orders.
The client can ask:
query {
customer(id: 1) {id
name orders {
id
amount
status
}
}
}
The response:
{
"data": {
"customer": {
"id": "1",
"name": "Manish",
"orders": [
{
"id": "101",
"amount": 500.0,
"status": "PAID"
},
{
"id": "102",
"amount": 1200.0,
"status": "CREATED"
}
]
}
}
}One query produced a nested response.
52. What happens internally?
When the query arrives:
query {
customer(id: 1) {
id
name
orders {
id
amount
}
}
}Spring GraphQL roughly follows this path:
HTTP POST /graphql
|
↓
GraphQL Engine
|
↓
Parse query
|
↓
Validate against schema
|
↓
Resolve customer
|
↓
@QueryMapping customer()
|
↓
Customer object
|
↓
Resolve orders
|
↓
@SchemaMapping orders()
|
↓
OrderRepository
|
↓
ResponseThe schema acts as the contract that determines whether the incoming query is valid.
53. GraphQL execution is field-oriented
This is an important architectural idea.
The GraphQL engine doesn’t simply call one Java method and serialize its result.
It resolves the requested fields.
For:
customer {
name
orders {
amount
}
}the engine needs to resolve:
customer
├── name
└── orders
└── amountThis field-oriented execution model is why nested resolvers and DataLoader become important.
54. GraphQL schema design matters
One mistake is to think:
“I’ll expose all my database tables directly through GraphQL.”
That’s rarely a good API design.
Your database model and API model don’t have to be identical.
For example, your database may contain:
customer
customer_internal_notes
customer_risk
customer_security_databut your public GraphQL API might expose only:
type Customer {
id: ID!
name: String!
email: String!
}The GraphQL schema is an API contract, not a database dump.
55. GraphQL and DTOs
In production applications, you may not want to expose JPA entities directly.
Instead:
GraphQL
↓
DTO
↓
Service
↓
Repository
↓
DatabaseFor example:
public record CustomerResponse(
Long id,
String name,
String email
) {
}Then your service can convert:
Entity → DTOThis provides more control over the API contract.
It also prevents persistence concerns from leaking into your GraphQL layer.
56. Recommended production structure
For a larger application, I’d use something closer to:
graphql
|
+-- controller
|
+-- resolver
|
+-- dtocustomer
|
+-- controller/service
+-- repository
+-- entityorder
|
+-- service
+-- repository
+-- entitycommon
|
+-- exception
+-- security
+-- configuration
The exact package structure can vary, but keeping the GraphQL API layer separate from the domain and persistence layers helps as the application grows.
57. GraphQL security concerns
GraphQL introduces some security concerns that are easy to miss.
A malicious or poorly designed query could request deeply nested data:
customer
↓
orders
↓
customer
↓
orders
↓
customer
↓
...You therefore need to think about:
- query depth
- query complexity
- maximum page size
- authorization
- rate limiting
- introspection policies
- batching
- expensive nested queries
GraphQL gives clients considerable flexibility, which means the server needs appropriate controls.
58. GraphQL query complexity
Imagine an API where a single query can trigger thousands of database operations.
A request such as:
query {
customers {
orders {
payments {
transactions {
merchant
}
}
}
}
}may look innocent but could result in a huge amount of work.
Production GraphQL APIs often apply limits around query depth, complexity, pagination, and execution cost.
59. GraphQL caching
Caching GraphQL responses isn’t exactly the same as caching simple REST URLs.
A REST cache might see:
GET /customers/101and cache the response.
GraphQL may have:
query {
customer(id: 101) {
id
name
}
}and:
query {
customer(id: 101) {
id
name
email
}
}These are different query documents.
Caching therefore needs to be considered at several levels:
Client cache
↓
DataLoader cache
↓
Application cache
↓
Database cacheThe right strategy depends on the application.
60. GraphQL subscriptions
GraphQL also supports subscriptions for real-time updates.
Imagine a payment dashboard.
The client wants:
Payment status changesA subscription could conceptually look like:
subscription {
paymentStatusChanged(paymentId: "TX100") {
paymentId
status
}
}The server could push:
{
"paymentId": "TX100",
"status": "COMPLETED"
}Subscriptions are useful for real-time applications, although their transport and infrastructure requirements need to be considered separately from ordinary query/mutation handling.
61. GraphQL in a microservices architecture
Suppose you have:
Customer Service
Order Service
Payment Service
Notification ServiceA frontend may need:
Customer
Orders
Payment statusWithout an aggregation layer, the frontend might need to communicate with multiple services.
GraphQL can act as an API composition layer:
Frontend
|
↓
GraphQL Gateway
/ | \
/ | \
↓ ↓ ↓
Customer Order Payment
Service Service ServiceThe GraphQL layer can compose data from multiple backend services.
This is one of the more interesting production use cases for GraphQL.
62. GraphQL is not automatically a microservices gateway
However, GraphQL doesn’t magically solve distributed-system problems.
If your GraphQL resolver makes:
10 service callsfor every request, your API may become slower rather than faster.
You still need to think about:
- service latency
- timeouts
- retries
- caching
- batching
- circuit breakers
- authorization
- partial failures
- observability
GraphQL changes the API interface. It doesn’t remove distributed-system complexity.
63. GraphQL and observability
For production systems, monitor at least:
GraphQL operation
GraphQL field
Execution time
Database queries
Downstream service calls
Error rate
Query depth
Query complexityFor example:
Operation: CustomerWithOrders
Latency: 82 mscustomer resolver: 10 ms
orders resolver: 55 ms
database: 45 msThis can help identify expensive resolvers.
64. Testing GraphQL
Spring for GraphQL provides testing support.
You can test an operation rather than calling the HTTP endpoint manually.
For example, a test can execute:
query {
customer(id: 1) {
id
name
}
}and verify:
customer.id
customer.nameThis gives you confidence that both the schema and resolver behavior are working correctly.
A good test suite should cover:
Valid queries
Invalid queries
Mutations
Validation
Authorization
Null handling
Nested fields
Error handling
Pagination65. A production-minded GraphQL architecture
For a larger Spring Boot application, you might end up with:
Client
|
↓
API Gateway
|
↓
GraphQL API
|
+-----------+-----------+
| | |
↓ ↓ ↓
Customer Order Payment
Service Service Service
| | |
↓ ↓ ↓
DB DB DBInside the GraphQL service:
GraphQL Schema
↓
Resolvers
↓
Application Services
↓
DataLoaders
↓
Repositories / Downstream APIsSecurity sits across the request path:
Authentication
↓
Authorization
↓
GraphQL
↓
Resolvers
↓
Services66. REST or GraphQL?
Neither approach is universally better.
GraphQL can be particularly useful when:
- clients need different data shapes
- there are many related resources
- frontend teams need flexibility
- mobile clients need to minimize requests
- multiple backend services need to be composed
- the API has a complex data graph
REST can be simpler when:
- resources map cleanly to endpoints
- operations are straightforward
- HTTP semantics are important
- caching through HTTP infrastructure is central
- clients don’t need highly flexible response shapes
Many organizations use both.
For example:
External API
↓
GraphQLInternal service-to-service
↓
REST / gRPC / messagingThe architecture should follow the use case rather than choosing GraphQL simply because it is newer.
67. Common mistakes when learning GraphQL
Mistake 1: Thinking GraphQL is just another REST framework
It isn’t.
GraphQL introduces a schema and a query execution model.
Mistake 2: Exposing database entities directly
Your API schema should represent business concepts rather than simply mirroring tables.
Mistake 3: Ignoring N+1 queries
Nested fields can produce large numbers of database calls.
DataLoader and batching become important.
Mistake 4: Returning unlimited lists
Always think about pagination for potentially large collections.
Mistake 5: Ignoring authorization at the field level
One GraphQL endpoint can expose many different pieces of data.
Authorization needs to follow the data, not just the URL.
Mistake 6: Allowing unlimited query complexity
A flexible API can also become an expensive API if clients are allowed to request arbitrarily deep or large queries.
68. The complete mental model
If you’re coming from Spring MVC, think about the difference this way.
REST:
HTTP Request
↓
Controller
↓
Service
↓
Repository
↓
DatabaseGraphQL:
GraphQL Request
↓
Schema Validation
↓
GraphQL Execution
↓
Resolver
↓
Service
↓
Repository / API
↓
DatabaseThe major change is the layer between the request and your business logic.
GraphQL introduces a typed schema and an execution engine that resolves the requested fields.
69. The complete small project
Our project now supports:
Query a customer
query {
customer(id: 1) {
id
name
email
}
}Query customers
query {
customers {
id
name
email
}
}Create a customer
mutation {
createCustomer(
name: "Manish"
email: "manish@example.com"
) {
id
name
email
}
}Query customer orders
query {
customer(id: 1) {
id
name orders {
id
amount
status
}
}
}At this point, you have a working Spring Boot + GraphQL + PostgreSQL application.
70. Where to go next
Once this project is working, I’d extend it in this order:
Phase 1
Basic queries
↓
Phase 2
Mutations
↓
Phase 3
Input types + variables
↓
Phase 4
Nested resolvers
↓
Phase 5
DataLoader
↓
Phase 6
Pagination
↓
Phase 7
Exception handling
↓
Phase 8
Spring Security
↓
Phase 9
Field-level authorization
↓
Phase 10
Caching
↓
Phase 11
Subscriptions
↓
Phase 12
Testing
↓
Phase 13
Observability
↓
Phase 14
GraphQL + microservices
↓
Phase 15
Production hardeningThat progression takes you from simply knowing GraphQL syntax to understanding how GraphQL actually fits into a Spring-based production system.
71. Final takeaway
The easiest way to understand Spring for GraphQL is to separate three things.
GraphQL
Defines how clients describe the data they want.
query
mutation
subscriptionGraphQL Schema
Defines what the API allows.
type Customer {
id: ID!
name: String!
email: String!
}Spring for GraphQL
Connects that schema to your Java application.
@QueryMapping
public Customer customer(@Argument Long id) {
...
}The complete flow is:
Client
|
| GraphQL Query
↓
GraphQL Endpoint
|
↓
Schema Validation
|
↓
GraphQL Execution
|
+---------+---------+
| |
↓ ↓
@QueryMapping @SchemaMapping
| |
+---------+---------+
|
↓
Service Layer
|
+---------+---------+
| |
↓ ↓
Repository Downstream API
| |
↓ ↓
Database MicroserviceThe most important thing to learn isn’t the annotations.
It is the relationship between:
Schema
↓
Query
↓
Resolver
↓
Service
↓
Data sourceOnce that becomes natural, concepts such as mutations, nested fields, DataLoader, pagination, authorization, subscriptions, and GraphQL federation become much easier to understand.
And if you’re coming from a Spring Boot + microservices background, GraphQL becomes particularly interesting when you start using it as a composition layer over multiple services rather than treating it as simply another way to expose a CRUD API.




