Definition
- GraphQL – a query language and runtime for APIs where the client specifies exactly which fields it needs, and the server returns precisely that shape in a single request.
- Over-fetching – a REST endpoint returns more fields than the client needs, wasting bandwidth.
- Under-fetching – the client must call several REST endpoints to assemble one view, adding latency.
- Resolver – a function backing each field in the schema that knows how to fetch that field's data.
- DataLoader – a batching utility that groups per-item fetches into a single query, solving the N+1 problem.
- Schema – a strongly typed definition of types, queries, mutations, and subscriptions that is self-documenting.
Rajesh wants to see on one screen his account balance, his last three transactions, and his linked card limit. With REST, he visits three counters, waits in three queues, and each counter gives him a thick form full of fields he did not ask for. With GraphQL, Rajesh writes one note saying exactly what he wants and hands it to a single counter that returns exactly those fields in one neat form.
Understanding the Concepts
What is GraphQL?
GraphQL exposes a single endpoint, typically /graphql, and the client sends a query describing the exact shape of data it wants. The server has a strongly typed schema defining types, fields, relationships, queries for reading, mutations for writing, and subscriptions for real-time updates. The response mirrors the query's shape exactly.
query {
account(number: "SAV2001") {
balance
transactions(last: 3) { amount type }
card { limit }
}
}
{
"data": {
"account": {
"balance": 150000.00,
"transactions": [
{ "amount": 5000.00, "type": "DEBIT" },
{ "amount": 25000.00, "type": "CREDIT" },
{ "amount": 1500.00, "type": "DEBIT" }
],
"card": { "limit": 200000.00 }
}
}
}
How does GraphQL solve over-fetching and under-fetching?
Over-fetching happens when GET /accounts/SAV2001 returns twenty fields but the mobile screen needs only the balance. Under-fetching happens when assembling one screen needs the account, then its transactions, then its card: three REST round trips.
GraphQL solves both: the client names only the fields it wants across related types and receives exactly that in one request.
// REST over-fetching – 20 fields returned, 1 needed:
GET /api/v1/accounts/SAV2001 → { id, holderName, balance, branchCode, ifsc, ... }
// REST under-fetching – 3 calls for one screen:
GET /api/v1/accounts/SAV2001
GET /api/v1/accounts/SAV2001/transactions?last=3
GET /api/v1/accounts/SAV2001/card
// GraphQL – one call, exact fields:
POST /graphql → { account { balance, transactions(last:3) { amount }, card { limit } } }
How do resolvers and DataLoader work?
Each field in the schema is backed by a resolver: a function that knows how to fetch that field's data. The N+1 problem occurs when fetching a list of accounts and then each account's card one by one.
package com.bank.graphql;
import org.springframework.graphql.data.method.annotation.*;
@Controller
public class AccountResolver {
@QueryMapping
public Account account(@Argument String number) {
return accountService.findByNumber(number);
}
@SchemaMapping
public List<Transaction> transactions(Account account, @Argument int last) {
return transactionService.lastN(account.getNumber(), last);
}
@SchemaMapping
public Card card(Account account) {
return cardService.findByAccount(account.getNumber());
}
}
DataLoader batches per-item fetches into a single query:
// Without DataLoader: N accounts → N card queries (N+1 problem)
// With DataLoader: N accounts → 1 batched card query
@SchemaMapping
public CompletableFuture<Card> card(Account account, DataLoader<String, Card> cardLoader) {
return cardLoader.load(account.getNumber());
}
What are the strengths of GraphQL?
A single request serves many client shapes, so a mobile app and a web dashboard can ask for different field sets from the same graph without building bespoke endpoints. The typed schema is self-documenting and enables autocompletion and compile-time client checks. Adding fields to the schema does not break existing clients. Deprecating fields is explicit with @deprecated.
What are the costs and weaknesses?
HTTP caching is harder because everything is a POST to one URL, so you lose the simple URL-based caching REST enjoys. Rate limiting is harder because a single query can be arbitrarily expensive. The N+1 problem requires disciplined DataLoader usage.
// Mandatory: limit query depth and complexity
package com.bank.config;
import graphql.analysis.MaxQueryDepthInstrumentation;
import graphql.analysis.MaxQueryComplexityInstrumentation;
import org.springframework.context.annotation.Bean;
@Configuration
public class GraphQLConfig {
@Bean
public MaxQueryDepthInstrumentation maxDepth() {
return new MaxQueryDepthInstrumentation(10); // max 10 levels deep
}
@Bean
public MaxQueryComplexityInstrumentation maxComplexity() {
return new MaxQueryComplexityInstrumentation(200); // max 200 complexity points
}
}
When should you use GraphQL vs REST? (comparison table)
| Scenario | GraphQL | REST |
|---|---|---|
| Many clients with diverse data needs | Ideal | Needs many endpoints |
| Simple resource CRUD | Overkill | Ideal |
| HTTP caching needed | Harder (POST to one URL) | Native (URL-based) |
| Browser-friendly public API | Needs tooling | Universal |
| Richly connected data graph | Natural fit | Needs joins/includes |
| File uploads, streaming | Awkward | Natural |
What are common mistakes?
The most common mistake is not adding query depth and complexity limits. Without them, a malicious client can send a deeply nested query that hammers the database.
// Malicious deep query – without limits this kills the database:
query {
account(number: "SAV2001") {
transactions {
account {
transactions {
account {
transactions { ... } // infinite depth
}
}
}
}
}
}
Another mistake is not using DataLoader, which turns every list query into an N+1 problem.
What should a senior engineer know?
Many organizations run both REST and GraphQL: REST for public and simple internal services, GraphQL as an aggregation layer for complex client-facing views. GraphQL moves complexity from the client to the server and to query governance. In a regulated banking context, the query-cost controls are not optional: they are mandatory to prevent denial of service. A senior engineer also knows that GraphQL subscriptions over WebSocket provide real-time updates, and that schema stitching or federation lets you compose multiple service schemas into one gateway graph.
Visual Overview
REST (multiple calls, fixed shapes):
┌────────┐ ┌───────────┐ ┌───────────┐
│ Client │─────→│ /accounts │─────→│ /txns │─────→ 3 round trips
└────────┘ └───────────┘ └───────────┘
┌───────────┐
│ /cards │
└───────────┘
GraphQL (one call, exact shape):
┌────────┐ ┌──────────────────────────────┐
│ Client │─────→│ POST /graphql │
│ query: │ │ ┌──────────┐ ┌───────────┐ │
│ { account { │ │ account │──│ txns │ │ 1 round trip
│ balance │ │ resolver │ │ resolver │ │
│ txns(3) │ └──────────┘ └───────────┘ │
│ card{limit}}│ ┌───────────┐ │
│ │ │ card │ │
│ │ │ resolver │ │
└────────┘ └──────────────────────────────┘
Key Takeaway
GraphQL is a query language and runtime for APIs where the client specifies exactly which fields it needs against a strongly typed schema, and the server returns precisely that shape in a single request through one endpoint. It cures two REST problems: over-fetching, where a REST endpoint returns more fields than a screen needs, and under-fetching, where assembling one view requires several REST calls.
On the server, each field is backed by a resolver that fetches that field's data, and GraphQL stitches the results into the requested shape. The main performance trap is the N+1 problem, where fetching a list and then each item's relation one by one explodes into many queries. The fix is a DataLoader that batches those fetches into a single query. GraphQL's strengths are that one graph serves many client shapes without bespoke endpoints, the typed schema is self-documenting, and adding fields does not break existing clients.
Its costs are that HTTP caching is harder because everything is a POST to one URL, rate limiting is harder because a single query can be arbitrarily deep and expensive, and simple CRUD is more machinery than REST needs. At National Bank of India, the mobile dashboard needed balance, recent transactions, and the linked card limit on one screen. With REST that was three round trips. A GraphQL layer let the mobile client ask for exactly that shape in one request, with DataLoader to batch card lookups and depth limits to prevent denial-of-service.
The decision rule is clear: use GraphQL when many clients have diverse, evolving data needs over a richly connected graph. Use REST when the API is simple, resource-oriented, and benefits from HTTP caching. Often run both: REST for simple services and GraphQL as an aggregation layer for complex client views.
Remember one sentence: GraphQL asks for exactly what you need in one trip, but the server must govern query depth and cost to keep that flexibility safe.
---