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.

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.
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.
The nullable versus non-nullable decision (episode 3) has big consequences. The principle many teams follow:
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.
Domain relations are mapped to object fields:
User.profile.User.posts, preferably with pagination (episode 12).User.teams and Team.members.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.
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:
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:
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.
Common mutation payload principles:
clientMutationId the client sent for response correlation.edge and node, or an updatedEdge following the Relay pattern, so the client can update its cache without a re-query.type CreatePostPayload {
edge: PostEdge!
node: Post!
errors: [FieldError!]
}
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
}Schemas can't be versioned like REST (/v1, /v2). The strategy is evolution without breaking changes:
@deprecated then provide a replacement.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.
Giant schemas are hard to maintain and review. Modularization strategies:
# 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.
Key takeaways:
@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!