Learn GraphQL - Advanced Schema Design Patterns
Episode 18 of 51

Learn GraphQL - Advanced Schema Design Patterns

Episode 18 covers advanced schema design: thinking in graphs, designing one-to-one through many-to-many relations, global object identification with the Node interface, mutation payload design, API evolution strategies with @deprecated, and schema modularization and domain-driven schemas.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

The schema is the most important architectural decision in a GraphQL application. Once released to clients, it's hard to change. Episode 18 covers the advanced schema design patterns experienced teams use to design schemas that stand the test of time.

We'll learn design principles, how to model relations, global object identification, mutation payload design, API evolution strategies, and schema modularization techniques.

Schema Design Principles

Thinking in Graphs

GraphQL forces you to think in graphs: entities as nodes, relations as edges. Before writing SDL, map your domain as a graph — what are the nodes, what are the relations, and from which node is it most natural for clients to start exploring.

A key principle: design the schema for client needs, not for the database structure. Keep the database normalized, but arrange the graph so clients can fetch data naturally.

Nullability: A Design Decision

The nullable versus non-nullable decision (episode 3) has big consequences. The principle many teams follow:

  • Non-null fields at the root: values that truly can't be empty.
  • Nullable fields on objects: values that could fail to fetch or are genuinely optional.

Be careful with chained non-null: if Post.author is non-null and the author resolver fails, the whole post fails too. Use non-null boldly only on fields that are truly guaranteed.

Connection Design

Modeling Relations

Domain relations are mapped to object fields:

  • One-to-one: a direct field, for example User.profile.
  • One-to-many: a list field, for example User.posts, preferably with pagination (episode 12).
  • Many-to-many: a two-way list field, for example User.teams and Team.members.
A many-to-many relation
type Team {
  id: ID!
  name: String!
  members: [TeamMember!]!
}
 
type TeamMember {
  user: User!
  role: String!
  joinedAt: DateTime!
}

Circular references (like User.teams and Team.members) are safe in GraphQL as long as queries don't have to include every level. GraphQL serves cyclic graphs without problems.

Global Object Identification

The Node Interface and Global Unique IDs

Relay defines the global identification pattern: every object has a unique global ID, and a Node interface lets you fetch any object by ID alone:

The Node interface
interface Node {
  id: ID!
}
 
type User implements Node {
  id: ID!
  username: String!
}
 
type Query {
  node(id: ID!): Node
}

A global ID usually carries type information (for example, the result of encoding Buffer.from("User:123").toString("base64")), so the server can determine the type and fetch the right object:

JSGlobal node resolver
Query: {
  node: async (_, args, ctx) => {
    const [type, id] = Buffer.from(args.id, "base64").toString().split(":");
    if (type === "User") return ctx.loaders.userById.load(id);
    if (type === "Post") return ctx.loaders.postById.load(id);
    return null;
  },
},

The benefits: clients can refetch any object through one uniform API, and the client cache (episode 20) gets a stable global ID key for normalization.

Mutation Payload Design

Consistency and Client-Side ID

Common mutation payload principles:

  • A mutation response is always an object (payload), not a scalar or direct object — this leaves room for errors (episode 11).
  • When needed, return the clientMutationId the client sent for response correlation.
  • After a mutation that changes a list, return the changed edge and node, or an updatedEdge following the Relay pattern, so the client can update its cache without a re-query.
A consistent mutation payload
type CreatePostPayload {
  edge: PostEdge!
  node: Post!
  errors: [FieldError!]
}
 
type Mutation {
  createPost(input: CreatePostInput!): CreatePostPayload!
}

API Evolution

Deprecation and Changes

Schemas can't be versioned like REST (/v1, /v2). The strategy is evolution without breaking changes:

  • Adding a new field: always safe (non-breaking).
  • Changing a field: use @deprecated then provide a replacement.
  • Removing a field: wait until all clients have migrated, then remove it in a major release.
Deprecating a field
type User {
  id: ID!
  username: String!
  fullName: String
    @deprecated(reason: "Gunakan nama field kombinasi first_name dan last_name")
  firstName: String
  lastName: String
}

Potentially breaking changes (like changing a type or adding non-null) are detected automatically by tools such as GraphQL Inspector and Apollo Studio schema checks — covered in episodes 21 and 32.

Schema Modularization

Breaking Down Large Schemas

Giant schemas are hard to maintain and review. Modularization strategies:

  • Schema stitching and type merging to combine multiple schemas (episode 22).
  • Extend types to add fields to existing types from other modules.
  • Domain-driven organization: split the schema per domain (user, order, inventory) like the folder structure in episode 6.
Extending a type across modules
# modul order.graphql
extend type User {
  orders(first: Int): OrderConnection!
}

Each module is owned by its own team and merged at the final composition. For large scale, federation in episodes 22 and 36 provides stronger per-service schema ownership.

Conclusion

Key takeaways:

  • Design schemas for client needs, not database structure.
  • Non-null has cascading consequences; use it deliberately.
  • One-to-one, one-to-many, and many-to-many relations are modeled as object fields.
  • The Node interface with global IDs enables refetching any object.
  • Consistent mutation payloads leave room for errors and ease cache updates.
  • Evolve APIs via @deprecated; modularize schemas by domain.

In the next episode, episode 19, you'll learn about optimizing GraphQL performance — query analysis and bottleneck identification, caching strategies, Automatic Persisted Queries, field-level caching with memoization, batching and DataLoader optimization, and performance monitoring. Your server will serve queries much faster!

Learn GraphQL - Advanced Schema Design Patterns | Learn GraphQL