Skip to content

GraphQL API Design ​

GraphQL lets clients select the fields they need through one typed schema. This flexibility becomes an operational risk unless schema governance, resolver cost, authorization, and query complexity are controlled.

Quick Decision ​

NeedGraphQL approachWatch out
Different screens need different fieldsOne endpoint with a typed schemaBound query cost
Command operationMutationDefine idempotency and errors
Live updatesSubscriptionMeasure connection and fan-out cost
Multiple domainsFederation or compositionManage ownership and breaking changes

Production Checklist ​

  • Problem: Does GraphQL actually reduce endpoint growth or over-fetching?
  • Solution: Are schema owner, resolver timeout, auth scope, complexity limit, and cache strategy clear?
  • Trade-off: Client flexibility increases; query cost and observability become more complex.
  • Failure mode: Handle partial data, resolver timeouts, downstream failures, and subscription disconnects.
  • Measurement: Track operation name, query complexity, resolver latency, error paths, and active connections.
  • Security/cost: Tie introspection, depth, alias, and batch-abuse controls to production policy.

Schema-First Design ​

The schema is the primary client/server contract. Types, fields, nullability, enums, inputs, and error behavior are designed together:

graphql
type Order {
  id: ID!
  status: OrderStatus!
  items: [OrderItem!]!
}

type Query {
  order(id: ID!): Order
  orders(after: String, first: Int = 20): OrderConnection!
}

type Mutation {
  cancelOrder(input: CancelOrderInput!): CancelOrderPayload!
}

Adding a field is usually backward compatible; deleting a field, changing nullability, or changing its meaning may be a breaking change. With federation, each subgraph should own its fields and manage shared entity IDs through a versioned contract.

Resolvers, Mutations, and Subscriptions ​

A resolver does more than fetch data: it carries auth context, timeouts, batching, and error translation. Avoid long synchronous chains; use bounded parallelism and downstream deadlines.

Mutation results should include the processed resource state, a stable error code, and an idempotency result when applicable. For long work, return a job ID and direct the client to a status query or webhook flow.

Design subscriptions with connection identity, authorization, heartbeat, reconnect, backpressure, tenant isolation, and broker fan-out. A disconnect may mean a lost or duplicated event; document event IDs and resume behavior.

DataLoader and N+1 ​

If a list resolver performs one downstream call per item, it creates N+1. DataLoader batches keys within one request scope. It must not become a global cache, or user/tenant isolation can break.

java
@BatchMapping
Map<String, List<OrderItem>> items(List<Order> orders) {
    return itemService.findByOrderIds(
        orders.stream().map(Order::getId).toList());
}

Measure batch size, database IN limits, cache lifetime, and resolver timeout. Adding DataLoader alone does not fix a bad query plan.

Directives, Authorization, and Complexity ​

Directives can mark deprecations, field masking, tenant filtering, and policy metadata. Authorization must apply at sensitive field and object levels, not only at the root query.

Use depth limits, field cost, timeout, maximum aliases, pagination caps, and persisted-query allowlists together. Requiring operation names makes tracing and cost analysis useful.

Caching and Federation ​

Field-level cache, normalized client cache, response cache, and persisted query/APQ are different layers. Document mutation invalidation, user/tenant scope, and acceptable staleness.

Federation distributes schema ownership but adds network hops, cross-subgraph failures, composition checks, and deployment-order concerns. Without clear domain boundaries, federation creates a distributed monolith.

Outcome ​

GraphQL success is measured by schema evolution, resolver budgets, field-level authorization, N+1 control, and operation-level observability. It can coexist with REST; client needs and operational cost must drive the decision.

Created by Eren Demir.