ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

GraphQL Architecture

Design production GraphQL APIs that scale. Covers schema design, resolvers, N+1 problem, DataLoader, subscriptions, federation, caching, and the patterns that make GraphQL fast and maintainable.

GraphQL lets clients request exactly the data they need — no more, no less. Unlike REST where the server determines the response shape, GraphQL clients specify exactly which fields they want. This eliminates over-fetching and under-fetching, but introduces new complexity in backend design.


GraphQL vs REST

REST:
  GET /api/users/123         → Full user object (30 fields)
  GET /api/users/123/orders  → All orders (separate request)
  GET /api/orders/456        → Full order object (20 fields)
  
  Problems:
  - Over-fetching: 30 fields returned, client needs 3
  - Under-fetching: Need 3 requests for related data
  - Multiple round trips for related data

GraphQL:
  query {
    user(id: "123") {
      name
      email
      orders(last: 5) {
        id
        total
        status
      }
    }
  }
  
  Single request, exactly the data needed, one round trip

Schema Design

# Schema-first design (SDL)
type Query {
  user(id: ID!): User
  orders(filter: OrderFilter, first: Int, after: String): OrderConnection!
}

type Mutation {
  createOrder(input: CreateOrderInput!): CreateOrderPayload!
  cancelOrder(id: ID!): CancelOrderPayload!
}

type User {
  id: ID!
  name: String!
  email: String!
  orders(first: Int, after: String): OrderConnection!
  createdAt: DateTime!
}

type Order {
  id: ID!
  total: Money!
  status: OrderStatus!
  items: [OrderItem!]!
  customer: User!
  createdAt: DateTime!
}

# Pagination: Relay cursor-based
type OrderConnection {
  edges: [OrderEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type OrderEdge {
  cursor: String!
  node: Order!
}

# Input types for mutations
input CreateOrderInput {
  items: [OrderItemInput!]!
  shippingAddress: AddressInput!
}

# Mutation payloads with errors
type CreateOrderPayload {
  order: Order
  errors: [UserError!]!
}

N+1 Problem & DataLoader

# WITHOUT DataLoader (N+1 problem):
# Query: { orders { customer { name } } }
# 1 query to get 100 orders
# 100 queries to get each customer (N+1!)

# WITH DataLoader:
from promise import Promise
from promise.dataloader import DataLoader

class CustomerLoader(DataLoader):
    def batch_load_fn(self, customer_ids):
        # Single query for ALL customers
        customers = Customer.objects.filter(id__in=customer_ids)
        customer_map = {c.id: c for c in customers}
        return Promise.resolve([
            customer_map.get(cid) for cid in customer_ids
        ])

# Resolver uses DataLoader
def resolve_customer(order, info):
    return info.context.customer_loader.load(order.customer_id)

# Result: 1 query for orders + 1 batched query for customers = 2 total

Caching

# Response caching with cache-control directives
type Query {
  # Public data, cache for 1 hour
  products: [Product!]! @cacheControl(maxAge: 3600, scope: PUBLIC)
  
  # Per-user data, cache for 5 minutes
  me: User! @cacheControl(maxAge: 300, scope: PRIVATE)
  
  # Real-time data, no cache
  orderStatus(id: ID!): OrderStatus! @cacheControl(maxAge: 0)
}

# Persisted queries (prevent arbitrary queries in production)
# Client sends hash, not full query
# POST /graphql { "id": "abc123", "variables": { "userId": "123" } }
# Server looks up query by hash from allowlist

Anti-Patterns

Anti-PatternConsequenceFix
No query depth limitingDenial of service via deep nestingSet max query depth (10-15)
No DataLoaderN+1 queries, slow responsesDataLoader for every batched resolver
Exposing database schemaTight coupling, security riskDomain-oriented schema design
No persisted queriesArbitrary query attacksPersisted/allowlisted queries in production
Single monolithic schemaTeam bottleneckSchema federation for ownership

GraphQL is a powerful API layer — but power requires discipline. Without depth limiting, DataLoader, and careful schema design, GraphQL can be slower and more dangerous than the REST API it replaced.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →