Course outline

GraphQL / Basic: language and response contracts / Lesson 5

Inputs, mutations and domain results

Design a bounded write contract with clear patch semantics and distinguish expected domain rejection from execution failure.

About 4 min readContent reviewed
On this page

Lesson goals

  • Separate input objects from output objects and define omission/null rules.
  • Explain serial root mutation execution without assuming a transaction.

Expose an intention rather than arbitrary storage editing

A mutation names a change the application supports, such as publishCourse or renameCourse. It should not accept every database column merely because the underlying model has those properties. GraphQL input objects describe permitted input structure and cannot be replaced with an output object type. A purpose-specific input leaves room for future optional fields while keeping the operation’s meaning clear. Type correctness is the first boundary; length, workflow state, ownership and conflict checks still belong to business validation before committing the change.

Make patch semantics visible

This input uses a required identifier and an optional summary. Its documented policy is that omission preserves the old summary, null clears it, and a supplied string replaces it after validation. The schema does not invent that policy automatically. The resolver must detect member presence instead of using a truthiness check that confuses an empty string with absence. The payload provides the updated object and an expected problem list. These are application design choices, not mandatory GraphQL field names or an automatic transaction implementation.

graphql An explicit edit contract
input EditCourseInput { id: ID!, summary: String }
type Problem { field: String!, code: String! }
type EditCoursePayload { course: Course, problems: [Problem!]! }
type Mutation { editCourse(input: EditCourseInput!): EditCoursePayload! }

The fragment assumes Course already exists. A client can distinguish a valid payload containing domain problems from transport or resolver failure.

Serial root fields do not create atomicity

GraphQL executes top-level mutation fields serially. That ordering does not wrap them in one database transaction, roll back an earlier success when a later field fails, or serialize every nested field as an independent write. Put one coherent business change behind one mutation and let the domain layer choose its transaction boundary. If publication also sends an external message, consider a durable delivery pattern and idempotent consumer under the storage design. Do not claim exactly-once effects merely because the client sent one mutation document.

Request the result needed to reconcile the client

A mutation response can return the canonical updated record, allowing a client to reconcile server normalization and generated defaults rather than assuming its submitted text is the final stored value. This operation clears summary using an explicit null variable member under the declared patch policy. If the application returns a validation problem instead, the client should present that domain result and avoid pretending a successful write occurred. Network failure requires a separate uncertain-outcome policy; it cannot always be converted into a safe automatic retry.

graphql Write with a reconciled result
mutation ClearSummary($input: EditCourseInput!) {
  editCourse(input: $input) {
    course { id summary }
    problems { field code }
  }
}

Variables {"input":{"id":"c1","summary":null}} request clearing. A successful fixture payload can return course.summary null with problems [].

Design expected rejection as part of the domain

A rejected title length or stale revision is often an expected business outcome that benefits from a stable machine code and field pointer. Unexpected source failures belong in execution error handling with appropriate disclosure control. There is no universal rule that every invalid action must be represented in one channel, but the chosen contract must be consistent for clients. Keep internal exception details private. Test authorization before mutation, validation without mutation, successful change, conflict and timeout-after-commit separately; a single happy-path fixture cannot establish the write protocol.

Common errors and fixes

An output type is used as an argument type

Why it happens
Input and output type categories were conflated.
How to fix it
Define a dedicated input object with allowed fields.
Prevent it next time
Validate the schema and review mass-assignment exposure.

Two mutation fields are assumed to roll back together

Why it happens
Serial execution was mistaken for a shared transaction.
How to fix it
Define one domain operation with a deliberate transaction boundary.
Prevent it next time
Test failure after the first effect.

An empty summary preserves old text unexpectedly

Why it happens
A truthiness check replaced member-presence handling.
How to fix it
Distinguish omission, null and supplied strings explicitly.
Prevent it next time
Keep patch fixtures for all three cases.

Check your understanding

1. Why should a mutation return the canonical updated object instead of only true?

Show worked solution

It lets clients observe the server’s final normalized values, identifiers and revision information and reconcile their caches. A Boolean can confirm an application-defined outcome but gives little information for updating an existing view. Return only fields appropriate for the caller and do not bypass normal output authorization merely because the object was just changed.

2. The first root mutation succeeds and the second fails. What must the client not assume?

Show worked solution

It must not assume that the first change rolled back. Serial execution provides an order, not a common transaction or external-effect rollback. Inspect the documented payload and error paths, reconcile the first operation’s state and use an operation-specific retry policy. If the two changes must be atomic, redesign the domain boundary rather than depending on their placement in one document.

Key takeaways

  • Input types describe structure; domain rules define permitted state changes.
  • Mutation ordering does not replace transactions or idempotency.

Official references