Why name a GraphQL mutation for the business action instead of the record write it performs?
answer
- Name the transaction, not the table write
- Optional everything erases the constraints
- One authorization rule per action
- Preconditions differ, so the mutations differ
- publishListing, not updateListing
basics
~20 sA mutation named for the write, such as updateListing, must accept every field change at once, so the schema cannot say which transitions are legal or who may make them. publishListing and withdrawListing carry that intent in the type system.
solid answer
~50 sNaming a mutation after the storage operation forces one field to absorb every possible change, and that has a type-system consequence: each input field must become optional, because no single change needs them all. The schema can then no longer say that reducing a price requires a reason, or that marking a listing sold requires a sale price — those rules move into resolver branches that inspect which keys are present. Authorization degrades the same way: a rule that an assistant may edit copy but not price becomes a runtime inspection of the input object rather than a rule attached to a field. Naming for the action — `publishListing`, `reducePrice`, `markListingSold` — gives each transaction exactly the arguments it needs, non-null where required, its own authorization rule, its own audit record and its own latency metric. None of this is specified; it is schema-design judgement.
code
graphql · 19 linesenum ListingStatus {
DRAFT
LIVE
UNDER_OFFER
SOLD
WITHDRAWN
}
input ListingInput {
title: String
askingPrice: Int
status: ListingStatus
soldPrice: Int
withdrawalReason: String
}
type Mutation {
updateListing(id: ID!, input: ListingInput!): Listing
}go deeper
Recall the contrast with a concrete pair: updateListing versus publishListing. Be able to say that the second one tells a reader what happened and the first one only says that something was written.
Explain the mechanism, not the taste. A broad update forces every input field to be nullable, which is what stops the schema from requiring a sale price on a sale, and pushes the rule into invisible resolver branches.
Demonstrate the operational consequences you have lived with: authorization spread across input inspection, audit trails that record no intent, and one latency metric covering five unrelated transactions.
Own the line itself. Define what counts as a business transaction for your domain, decide how the team resists both mutation explosion and the catch-all update, and say how that decision is enforced during schema review.
## Two ways to name the same capability Take a real-estate listings graph where a listing moves through `DRAFT`, `LIVE`, `UNDER_OFFER`, `SOLD` and `WITHDRAWN`, carries an asking price, and gains a sale price when it completes. One schema exposes the storage operation: ```graphql input ListingInput { title: String askingPrice: Int status: ListingStatus soldPrice: Int withdrawalReason: String } type Mutation { updateListing(id: ID!, input: ListingInput!): Listing } ``` The other exposes the transactions an agent actually performs: ```graphql type Mutation { publishListing(input: PublishListingInput!): PublishListingPayload! reducePrice(input: ReducePriceInput!): ReducePricePayload! markListingSold(input: MarkListingSoldInput!): MarkListingSoldPayload! withdrawListing(input: WithdrawListingInput!): WithdrawListingPayload! } ``` Both can perform the same changes. They differ in what the schema is able to *say*. ## The optionality collapse The first schema has one structural problem from which everything else follows: every field on `ListingInput` must be nullable. A caller changing only the title cannot be made to supply a status; a caller marking a listing sold cannot be made to supply a title. So the type system's ability to require anything is spent on the first line of the file. That matters because real constraints exist and now cannot be expressed. Marking a listing sold requires a sale price. Withdrawing requires a reason. Publishing requires that the listing has at least one photograph. In the action-named schema each of those is a non-null field on a small input type, visible in introspection, enforced during validation before a resolver runs, and reflected in whatever typed client is generated from the schema. In the broad schema, none of them is expressible, so all of them become runtime branches: *if `status` is `SOLD` and `soldPrice` is absent, throw*. The rules still exist; they have simply left the contract and become invisible to every caller until they fail. ## Authorization and audit follow the name Suppose an assistant account may edit descriptive copy but not price, and only a branch manager may withdraw a listing. With action-named mutations these are rules attached to fields: `reducePrice` and `withdrawListing` each carry their own check, and a reviewer can read the schema and see which entry points are privileged. With one `updateListing`, authorization becomes a function that inspects the submitted input object and decides which combinations of present keys the caller is allowed. That code is easy to get wrong, hard to test exhaustively, and impossible for a client to discover. The same collapse hits everything downstream that wants to know *what happened*. An audit trail full of `updateListing` entries has to reconstruct intent by diffing before-and-after snapshots. A metrics dashboard shows one latency and one error rate for an endpoint that does five unrelated things, so a regression in the slowest transaction is diluted by four fast ones. Domain events published from an action-named resolver can be named for the action; events published from a generic update usually degenerate into a single `listingChanged` that every consumer must re-interpret. ## Naming for the action, in practice A good mutation name is an imperative verb phrase describing something a person in the domain would say they did: `publishListing`, `reducePrice`, `acceptOffer`, `withdrawListing`. A poor one describes the mechanism: `updateListing`, `setListingStatus`, `patchListingFields`, `saveListing`. The test is whether the name survives a change of storage. If you moved listings to a different store tomorrow, `publishListing` still means exactly what it meant; `updateListing` was only ever a description of a row being written. A useful second test is the *precondition* test: if two changes have different preconditions, different authorization, or different side effects, they are two mutations however similar the rows they touch. Publishing sends notifications to watchers; reducing a price does not. That is the seam. ## The counter-pressure, and where the line sits The honest objection is mutation explosion: a graph with four hundred mutation fields is its own kind of unusable, and every one carries an input type and a payload type. So the line is not "one mutation per field" — it is one mutation per *business transaction*: a change that has its own preconditions, its own permission, or its own consequences. There is a legitimate place for a broad update. A settings form that edits an arbitrary bag of descriptive fields — title, blurb, viewing notes — with one permission and no side effects genuinely is one transaction, and splitting it into eleven mutations serves nobody. What makes the broad shape a defect is using it for changes that *do* differ: sweeping a status transition, a price change and a copy edit into the same field because they all happen to write the same table. ## What an interviewer is listening for They want to hear that this is a schema-design judgement rather than a specified rule, that you can name the type-system consequence rather than only the aesthetic one, and that you can state the cost of the position you are advocating. A candidate who says "always one mutation per action" without acknowledging mutation explosion has a slogan; a candidate who names the precondition seam has a rule they can apply.
- Where do you draw the line before the mutation root type has four hundred fields?At the business transaction, not the field. Two changes are two mutations when they have different preconditions, different authorization, or different side effects. A form that edits a bag of descriptive fields under one permission with no consequences is one transaction and deserves one mutation, even though it writes several columns.
- Is any of this naming guidance in the GraphQL specification?No. The specification defines the mutation root type and how its fields execute; it says nothing about what fields are called or how coarse they should be. Mutation naming is entirely a schema-design convention, and the only naming rule the specification does impose is that names may not begin with two underscores, which is reserved for introspection.
- What breaks in observability when every write goes through one update mutation?Per-operation metrics stop being per-action. One latency histogram and one error rate cover five unrelated transactions, so a regression in the slow one is diluted by the fast ones and an error spike names no cause. Audit records say only that something was updated, leaving consumers to reconstruct intent by diffing snapshots.
A control panel with one "change settings" box accepts any combination someone types, so every safety rule has to be checked after the fact. A panel with a labelled button per action can refuse the illegal move before it is made.
saying these in an interview costs you the question
- Names every mutation create, update or delete by reflex
- Says one update mutation is fine because fields are optional
- Puts each action's authorization in a runtime branch on the input
- Claims the GraphQL specification defines mutation naming
- Assumes more mutation fields is always a worse schema
- Cannot name a case where a broad update mutation is right