AlgoMaster Logo

GraphQL Schema Design

High Priority12 min readUpdated September 13, 2026
Listen to this chapter
Unlock Audio

Choosing GraphQL does not resolve the decisions that make a catalog easy or difficult to use. A field named status may still have several meanings. A required author biography may cause an otherwise useful book response to disappear. A generic JSON field may leave clients guessing which properties exist. The schema needs to make those choices clear.

Schema design turns these choices into an explicit contract.

This chapter explains how to model domain objects, choose field types, define nullability, separate inputs from outputs, and represent related kinds of objects without making the interface unnecessarily complex.

1. Domain Concepts and Relationships

Start with what consumers need to understand. For a public bookstore catalog, those concepts include books, authors, and whether customers can currently order a book. Internal database tables, supplier records, and synchronization jobs need not become public types.

Assume each Book represents one catalog edition. A paperback and an ebook have separate book IDs, even if their titles match. Authors have their own stable identities. This definition matters because clients use IDs to distinguish objects and associate cached data with them.

The graph below describes the initial read model:

The root lookup fields provide entry points. The authors relationship lets a client navigate from a book to its credited authors. It describes a public relationship, not a promise about how the server joins tables or calls services.

Expose relationships that serve a known use case. Adding every possible reverse link can create expensive traversal paths and leave clients unsure which route to use. An author's full bibliography, for example, needs a bounded collection design before it becomes a public field.

2. A Readable Schema Contract

Schema Definition Language, usually called SDL, is the textual notation for describing GraphQL types. The following schema is complete as a type definition, but it still needs server behavior to supply data.

Query is the conventional name for the root read type. A schema needs a query root; mutation and subscription roots are optional. GraphQL also supports an explicit schema definition that assigns different root type names.

The triple-quoted text contains schema descriptions. Tools can display these descriptions alongside types and fields. Explain meaning, ordering, units, and absence behavior rather than merely restating the field name.

Use PascalCase for type names, camelCase for fields and arguments, and uppercase enum values as a consistent naming convention. These casing choices are conventions, not language requirements. GraphQL reserves names beginning with __ for introspection. Do not use them for application-defined types and fields.

Prefer availability over a vague status, and biography over an implementation name such as profileBlob. A field name becomes part of the consumer's vocabulary. Choose one that still makes sense if storage changes.

3. Scalars, Enums, and Structured Values

A scalar represents a leaf value that clients cannot select subfields from. GraphQL provides five built-in scalars:

Scroll
ScalarMeaningDesign consideration
StringTextDoes not enforce non-empty content or a maximum length
IntSigned 32-bit integerUnsuitable for numbers outside that range
FloatDouble-precision floating-point valueAvoid for amounts that require exact decimal arithmetic
BooleanTrue or falsePrefer an enum when the domain has several named states
IDIdentifier that GraphQL serializes as a stringDoes not establish a uniqueness policy or grant access

For this catalog, return identifiers such as bk_1042 and au_27, and ask clients to treat them as opaque. GraphQL does not require these prefixes. The server's identity policy makes them stable; choosing ID alone does not.

An enum declares a finite set of named values. BookAvailability communicates more than an undocumented string or a pair of booleans such as isAvailable and isPreorder. Define AVAILABLE as orderable for normal fulfillment, OUT_OF_STOCK as temporarily unavailable for ordering, and PREORDER as accepting orders before release. These meanings belong to the bookstore contract.

The nullable availability field has a different purpose: null means the service could not determine the current state. Reporting OUT_OF_STOCK during a supplier outage would turn missing information into a false business claim.

Custom Scalars

A custom scalar adds an application-defined leaf format. For example, this extension adds a publication date to the catalog:

GraphQL does not include Date as a built-in scalar. The server must implement its parsing, validation, and serialization rules. A declaration and description do not enforce the format by themselves. Client generators also need an appropriate mapping for custom scalars.

Use an object when consumers need selectable components. An address with a city and postal code is usually more useful as a typed object than as an opaque scalar. A generic JSON scalar can be appropriate for intentionally unstructured content, but using one for the entire book removes much of the contract that GraphQL tooling can inspect.

4. Nullability and Failure Boundaries

GraphQL fields are nullable unless you wrap their types with !. On output, non-null is a promise about a returned value, not an instruction to invent a fallback. title: String! still permits an empty string unless the application prohibits it.

List nullability has two independent dimensions:

Scroll
TypeCan the whole list be null?Can an item be null?Can the list be empty?
[Author]YesYesYes
[Author!]YesNoYes
[Author]!NoYesYes
[Author!]!NoNoYes

Our authors: [Author!]! contract requires a list with no null author entries, but allows the list to be empty. Return [] for that legitimate state. An author lookup failure must not silently become an empty credit list.

Nullability also determines how failures affect surrounding data. If an author object cannot provide its selected non-null name, the error propagates through non-null positions until it reaches a nullable boundary. GraphQL documentation often calls this behavior null bubbling.

The diagram traces that failure through the catalog schema:

The title may have loaded successfully, yet the response cannot retain that book under these guarantees. By contrast, a failed nullable biography can leave the author's name and the book intact.

For this design, assume titles and credited author names come from maintained catalog records. The team accepts losing the book result if the server cannot supply those required values. If author data instead comes from an unreliable external service and partial book display is useful, consider a nullable author relationship before publishing the contract.

Choose non-null fields where consumers benefit from the guarantee and the failure behavior is acceptable. Avoid making every field required merely because today's database column has a NOT NULL constraint.

5. Arguments and Input Objects

Arguments let clients supply inputs to fields. book(id: ID!) accepts a required identifier and returns an optional match. The input requirement and output nullability answer different questions: the caller must provide an ID, but that ID may not identify an available book.

An input object groups related input fields. You cannot use output object types such as Book as argument types. Use scalars, enums, input objects, and permitted list/non-null combinations for inputs.

Suppose the bookstore needs a short title suggestion list. These definitions extend the catalog schema:

The input object keeps related filters together. limit controls the size of the result rather than which books match. This preview intentionally has no continuation mechanism; an interface for browsing every match needs a paginated collection contract.

If the caller omits limit, GraphQL uses the default 10. It rejects explicit null because the argument is non-null. Callers can omit non-null inputs that have a default.

Descriptions such as “1 to 20” do not add GraphQL validation rules. The server must enforce the range and the title length. These checks produce different outcomes:

Supplied inputOutcome
filter: {titleContains: "Reliable"}, no limitUse the default limit of 10
filter: {titleContains: "Reliable", availability: null}Apply no availability filter
filter: {}Reject the missing required titleContains field
limit: nullReject a null value for the non-null argument
limit: "ten"Reject the wrong input type
limit: 100Pass the GraphQL integer type check, then fail the application's range check

For the accepted filter, no matching books means []. It does not mean the lookup failed.

Omission and Explicit Null

Keep omission and null distinct when the business operation needs that distinction. A possible input for editing a book description is:

This is an input definition only, not a complete write operation. It illustrates why reusing output types or treating every missing value as null would lose useful meaning. The implementation must check whether the caller supplied description. A default on that field would change what happens when the caller omits it, so this definition declares no default.

An identifier in an input also does not prove permission to edit the record. Keep write inputs limited to supported changes, and enforce authorization separately.

6. Interfaces and Unions

Sometimes a field can return more than one kind of object. An interface describes shared fields that its implementing object types must provide. A union lists possible object types without declaring shared selectable fields.

The following is a separate, simplified schema alternative for a catalog that distinguishes books and audiobooks. It is not an extension to concatenate with the earlier Book definition:

A catalog item always offers id and title. A search result may instead be an author, so the union makes no shared field promise. Union members must be object types.

Clients can select common interface fields directly. To select subtype fields, use a type condition so the selection applies only to that object type. On a union, ordinary fields need these type conditions even when several members share a field name. The __typename meta-field identifies the concrete object type.

Use an interface when consumers need a meaningful common contract. Use a union when a result can be one of several different objects. Do not introduce a hierarchy just to deduplicate a few field declarations. The server must also identify the concrete runtime type correctly; SDL alone cannot do that work.

7. Reviewing the Consumer Contract

Read a proposed schema as a consumer before implementing every field. A short operation often exposes naming and nullability decisions more clearly than a long type listing. This operation uses the original catalog schema:

With bookId set to bk_1042, an ordinary successful response body could be:

Now consider the non-null author-name failure traced in the diagram. For the same operation, this is a representative response body; message wording is an implementation choice:

The path identifies the field that failed, including the zero-based author index, even though the returned book is null. An unknown book, by the lookup's documented policy, instead returns {"data":{"book":null}} without an execution error. Clients can distinguish normal absence from this failure.

Review authorization alongside the shape. This schema exposes public catalog data and no private supplier fields. If you add protected data, define its visibility and denial behavior before choosing non-null guarantees. A schema description cannot enforce access rules.

Finally, validate the SDL and representative operations, then check behavior the type system cannot express: ordering, length limits, missing-record policy, and identity stability. Treat field meanings and whether fields can be null as promises that clients will continue to rely on. A structurally valid schema is the beginning of a usable contract, not the whole contract.

Summary

Design GraphQL schemas around domain concepts and consumer needs. Use explicit types, meaningful names, and descriptions that explain business semantics. Choose scalars, enums, objects, interfaces, and unions according to the information clients need to understand and select.

Nullability defines both value guarantees and failure boundaries. Inputs need their own contracts, including defaults and the distinction between omission and null. Validate the schema while enforcing business constraints, authorization, and runtime behavior in the implementation.