Skip to main content

Schema design that scales with your org

Your schema is an API contract and an org chart. Design it for the consumers you want, not the databases you have.

Model the domain, not the tables

The single most common mistake we see in graph reviews: schemas that mirror the storage layer.

# ❌ Storage leaking into the API
type OrderRow {
order_id: ID!
cust_fk: ID!
status_code: Int!
}

# ✅ Domain modelling
type Order {
id: ID!
customer: Customer!
status: OrderStatus!
}

enum OrderStatus { PENDING PAID SHIPPED CANCELLED }

Consumers should never need to know your join keys or status integers. Enums over magic numbers, object references over foreign keys, always.

Nullability is a promise, not a default

Every non-null field (!) is a promise that this field can never fail independently. In a distributed graph, a non-null field whose resolver fails nulls out its whole parent chain — one flaky downstream service can blank an entire page.

Rules of thumb:

  • IDs and enums on the type itself: non-null.
  • Anything resolved from another service or datasource: nullable, even if it "should always exist".
  • Lists: prefer [Thing!] (nullable list, non-null items) so a failure returns null, not a list with holes.

Design for the client's screen, not for reuse

A field that tries to serve every consumer serves none well. It's fine — good, even — to have Product.cardSummary and Product.detailView style fields shaped for specific surfaces. Generic "one true type" schemas push formatting logic into every client and change constantly.

Naming conventions we enforce

RuleExample
Queries are nounsorder(id:), not getOrder
Mutations are verb-firstcancelOrder, addLineItem
Mutations return a payload typeCancelOrderPayload with the changed entity + user errors
Booleans read as assertionsisCancellable, not cancellable
No abbreviations in public fieldsquantity, not qty

Mutations: payload types with user errors

Don't throw GraphQL errors for business outcomes. Reserve top-level errors for system failures; model expected failures in the schema:

type CancelOrderPayload {
order: Order
userErrors: [UserError!]!
}

type UserError {
field: [String!]
message: String!
code: ErrorCode!
}

Clients can then build UI against typed, predictable failure modes — and your error monitoring stays clean for real incidents.

Review checklist

  • No storage identifiers, join keys or status integers in public types
  • Cross-service fields are nullable
  • Every mutation returns a payload type with userErrors
  • Enums for every closed set of values
  • Deprecations carry a reason with a migration path (see Versioning & deprecation)