Schema Design Patterns
Best practices and patterns for designing maintainable and scalable GraphQL schemas.
Schema Organization
Domain-Driven Design
# User domain
type User {
id: ID!
email: String!
profile: UserProfile!
posts: [Post!]!
followers: [User!]!
following: [User!]!
}
type UserProfile {
displayName: String!
bio: String
avatar: String
location: String
website: String
}
# Content domain
type Post {
id: ID!
title: String!
content: String!
author: User!
tags: [Tag!]!
publishedAt: DateTime
updatedAt: DateTime
status: PostStatus!
comments: [Comment!]!
likes: [Like!]!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
parent: Comment
replies: [Comment!]!
createdAt: DateTime!
}
# Taxonomy domain
type Tag {
id: ID!
name: String!
slug: String!
description: String
posts: [Post!]!
color: String
}
# Engagement domain
type Like {
id: ID!
user: User!
post: Post!
createdAt: DateTime!
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
Modular Schema Structure
# schemas/user.py
from graphene import ObjectType, String, List, Field
from .types import UserType, UserProfileType
class UserQueries(ObjectType):
user = Field(UserType, id=String(required=True))
users = List(UserType)
me = Field(UserType)
class UserMutations(ObjectType):
create_user = CreateUser.Field()
update_user = UpdateUser.Field()
delete_user = DeleteUser.Field()
# schemas/content.py
class ContentQueries(ObjectType):
post = Field(PostType, id=String(required=True))
posts = List(PostType)
search_posts = List(PostType, query=String(required=True))
class ContentMutations(ObjectType):
create_post = CreatePost.Field()
update_post = UpdatePost.Field()
delete_post = DeletePost.Field()
# schemas/main.py
class Query(UserQueries, ContentQueries, ObjectType):
pass
class Mutation(UserMutations, ContentMutations, ObjectType):
pass
schema = Schema(query=Query, mutation=Mutation)
Type Design Patterns
Interface and Union Types
# Node interface for Relay compliance
interface Node {
id: ID!
}
# Content interface for searchable content
interface Content {
id: ID!
title: String!
content: String!
author: User!
createdAt: DateTime!
updatedAt: DateTime!
}
# Timestamped interface
interface Timestamped {
createdAt: DateTime!
updatedAt: DateTime!
}
# Implementation
type Post implements Node & Content & Timestamped {
id: ID!
title: String!
content: String!
author: User!
createdAt: DateTime!
updatedAt: DateTime!
tags: [Tag!]!
status: PostStatus!
}
type Article implements Node & Content & Timestamped {
id: ID!
title: String!
content: String!
author: User!
createdAt: DateTime!
updatedAt: DateTime!
category: Category!
featured: Boolean!
}
# Union types for polymorphic queries
union SearchResult = Post | Article | User | Tag
union ActivityItem = PostCreated | CommentAdded | UserFollowed
type PostCreated {
id: ID!
post: Post!
user: User!
timestamp: DateTime!
}
type CommentAdded {
id: ID!
comment: Comment!
user: User!
timestamp: DateTime!
}
type UserFollowed {
id: ID!
follower: User!
following: User!
timestamp: DateTime!
}
Nested Input Types
input CreatePostInput {
title: String!
content: String!
tags: [TagInput!]
metadata: PostMetadataInput
schedule: ScheduleInput
}
input TagInput {
id: ID
name: String
create: Boolean = false
}
input PostMetadataInput {
description: String
keywords: [String!]
featured: Boolean = false
allowComments: Boolean = true
}
input ScheduleInput {
publishAt: DateTime
timezone: String = "UTC"
}
# Nested update inputs
input UpdatePostInput {
title: String
content: String
tags: UpdateTagsInput
metadata: UpdatePostMetadataInput
}
input UpdateTagsInput {
add: [TagInput!]
remove: [ID!]
replace: [TagInput!]
}
Pagination Patterns
Cursor-Based Pagination (Relay Style)
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PostEdge {
node: Post!
cursor: String!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type Query {
posts(
first: Int
after: String
last: Int
before: String
filter: PostFilter
sort: PostSort
): PostConnection!
}
Offset-Based Pagination
type PostPage {
items: [Post!]!
totalCount: Int!
pageNumber: Int!
pageSize: Int!
totalPages: Int!
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
type Query {
posts(
page: Int = 1
pageSize: Int = 10
filter: PostFilter
sort: PostSort
): PostPage!
}
Custom Pagination
input PaginationInput {
limit: Int = 10
offset: Int = 0
cursor: String
}
type PaginationInfo {
hasMore: Boolean!
nextCursor: String
total: Int
}
type PostList {
posts: [Post!]!
pagination: PaginationInfo!
}
Filtering and Sorting
Advanced Filtering
input PostFilter {
# Text search
search: String
title: StringFilter
content: StringFilter
# Author filtering
authorId: ID
authorIds: [ID!]
author: UserFilter
# Date filtering
createdAt: DateTimeFilter
publishedAt: DateTimeFilter
# Status filtering
status: PostStatus
statuses: [PostStatus!]
# Tag filtering
tags: TagFilter
hasAllTags: [String!]
hasAnyTags: [String!]
# Boolean filtering
featured: Boolean
allowComments: Boolean
# Logical operators
AND: [PostFilter!]
OR: [PostFilter!]
NOT: PostFilter
}
input StringFilter {
equals: String
contains: String
startsWith: String
endsWith: String
in: [String!]
notIn: [String!]
regex: String
isEmpty: Boolean
}
input DateTimeFilter {
equals: DateTime
before: DateTime
after: DateTime
between: DateTimeRange
}
input DateTimeRange {
start: DateTime!
end: DateTime!
}
input TagFilter {
id: ID
name: String
slug: String
}
input UserFilter {
id: ID
email: String
displayName: String
verified: Boolean
}
Sorting Patterns
enum PostSortField {
CREATED_AT
UPDATED_AT
PUBLISHED_AT
TITLE
AUTHOR_NAME
COMMENT_COUNT
LIKE_COUNT
VIEW_COUNT
}
enum SortDirection {
ASC
DESC
}
input PostSort {
field: PostSortField!
direction: SortDirection = ASC
}
input PostSortInput {
sorts: [PostSort!]!
}
# Multiple sort fields
type Query {
posts(sort: [PostSort!] = [{ field: CREATED_AT, direction: DESC }]): [Post!]!
}
Error Handling Patterns
Result Types Pattern
interface Result {
success: Boolean!
message: String
}
type PostResult implements Result {
success: Boolean!
message: String
post: Post
errors: [FieldError!]
}
type UserResult implements Result {
success: Boolean!
message: String
user: User
errors: [FieldError!]
}
type FieldError {
field: String!
message: String!
code: String
}
type Mutation {
createPost(input: CreatePostInput!): PostResult!
updatePost(id: ID!, input: UpdatePostInput!): PostResult!
}
Union Error Types
union CreatePostResult =
| Post
| ValidationError
| AuthenticationError
| ServerError
type ValidationError {
message: String!
field: String!
code: String!
}
type AuthenticationError {
message: String!
code: String!
}
type ServerError {
message: String!
code: String!
details: String
}
type Mutation {
createPost(input: CreatePostInput!): CreatePostResult!
}
Error Extensions
# Custom error with extensions
class CustomError(Exception):
def __init__(self, message, code=None, field=None):
super().__init__(message)
self.code = code
self.field = field
@property
def extensions(self):
extensions = {}
if self.code:
extensions['code'] = self.code
if self.field:
extensions['field'] = self.field
return extensions
# In resolver
def resolve_create_post(self, info, input):
try:
return create_post(input)
except ValidationError as e:
raise CustomError(
message=str(e),
code='VALIDATION_ERROR',
field=e.field
)
Security Patterns
Authorization Patterns
# Field-level authorization
type User {
id: ID!
email: String! # Only accessible by user or admin
profile: UserProfile!
posts: [Post!]!
privateNotes: String # Only accessible by user
}
# Role-based access
enum Role {
ADMIN
MODERATOR
USER
GUEST
}
type Query {
# Admin only
allUsers: [User!]! # @auth(requires: ADMIN)
# User or admin
user(id: ID!): User # @auth(requires: USER)
# Public
posts: [Post!]!
}
Input Validation
input CreateUserInput {
email: String! # @constraint(format: "email")
password: String! # @constraint(minLength: 8, maxLength: 128)
displayName: String! # @constraint(minLength: 2, maxLength: 50)
age: Int # @constraint(min: 13, max: 120)
website: String # @constraint(format: "url")
}
# Custom validation directives
directive @constraint(
minLength: Int
maxLength: Int
min: Int
max: Int
format: String
pattern: String
) on INPUT_FIELD_DEFINITION
directive @auth(requires: Role, policy: String) on FIELD_DEFINITION | OBJECT
Performance Patterns
DataLoader Pattern
from graphene import ObjectType, Field, List
from promise import Promise
from promise.dataloader import DataLoader
# DataLoader for N+1 problem
class UserLoader(DataLoader):
def batch_load_fn(self, user_ids):
users = User.objects.filter(id__in=user_ids)
user_map = {user.id: user for user in users}
return Promise.resolve([user_map.get(user_id) for user_id in user_ids])
class PostType(ObjectType):
author = Field(UserType)
def resolve_author(self, info):
return info.context.user_loader.load(self.author_id)
# Context setup
class Context:
def __init__(self):
self.user_loader = UserLoader()
def graphql_view(request):
context = Context()
return GraphQLView.as_view(schema=schema, context=context)(request)
Query Complexity Analysis
from graphql import validate, build_schema
from graphql.validation import ValidationRule
class QueryComplexityRule(ValidationRule):
def __init__(self, max_complexity=1000):
self.max_complexity = max_complexity
self.complexity = 0
def enter_field(self, node, *args):
# Calculate field complexity
field_complexity = getattr(node.type, 'complexity', 1)
self.complexity += field_complexity
if self.complexity > self.max_complexity:
raise Exception(f"Query complexity {self.complexity} exceeds limit {self.max_complexity}")
# Usage
validation_rules = [QueryComplexityRule(max_complexity=1000)]
errors = validate(schema, document, rules=validation_rules)
Caching Strategies
from functools import wraps
import hashlib
def cache_resolver(ttl=300):
def decorator(resolver):
@wraps(resolver)
def wrapper(root, info, **kwargs):
# Generate cache key
cache_key = f"{resolver.__name__}:{root.id if root else 'root'}:{kwargs}"
cache_key = hashlib.md5(cache_key.encode()).hexdigest()
# Try cache first
cached_result = cache.get(cache_key)
if cached_result:
return cached_result
# Execute resolver
result = resolver(root, info, **kwargs)
# Cache result
cache.set(cache_key, result, ttl)
return result
return wrapper
return decorator
class PostType(ObjectType):
comments = Field(List(CommentType))
@cache_resolver(ttl=600) # Cache for 10 minutes
def resolve_comments(self, info):
return Comment.objects.filter(post=self)
Subscription Patterns
Real-time Updates
type Subscription {
# Post updates
postCreated: Post!
postUpdated(id: ID!): Post!
postDeleted: ID!
# Comment updates
commentAdded(postId: ID!): Comment!
commentUpdated(id: ID!): Comment!
# User activity
userOnline: User!
userOffline: User!
# Chat messages
messageAdded(roomId: ID!): Message!
}
# Filtered subscriptions
type Subscription {
postUpdated(filter: PostSubscriptionFilter): PostUpdatePayload!
}
input PostSubscriptionFilter {
authorId: ID
tags: [String!]
status: PostStatus
}
type PostUpdatePayload {
post: Post!
operation: SubscriptionOperation!
previousValues: Post
}
enum SubscriptionOperation {
CREATED
UPDATED
DELETED
}
Subscription Implementation
import asyncio
from graphene import ObjectType, Field, String, Subscription
class PostSubscription(ObjectType):
post_created = Field(PostType)
async def resolve_post_created(root, info):
# Subscribe to post creation events
queue = asyncio.Queue()
# Register queue with event system
event_system.subscribe('post_created', queue)
try:
while True:
post_data = await queue.get()
yield post_data
finally:
event_system.unsubscribe('post_created', queue)
# Event system
class EventSystem:
def __init__(self):
self.subscribers = defaultdict(list)
def subscribe(self, event_type, queue):
self.subscribers[event_type].append(queue)
def unsubscribe(self, event_type, queue):
self.subscribers[event_type].remove(queue)
async def publish(self, event_type, data):
for queue in self.subscribers[event_type]:
await queue.put(data)
event_system = EventSystem()
This comprehensive guide covers essential schema design patterns for building maintainable, scalable, and secure GraphQL APIs.