Single vs. Multiple Mutations for Serial Workflow

My team has an end-user requirement to “activate a feature” and then “save a profile” through a single user action (form submission).

As we begin the schema design process, my initial thought was to create a single activateAndSaveProfile mutation within the feature set’s namespace. This would abstract the transactional (and serial) workflow into one GraphQL operation. However, I’m now considering whether it might be better to expose two separate mutations - activate and saveProfile -within the same namespace, allowing them to be called serially but independently (read more about namespaces for serial mutations).

Example: Single Mutation (nested in namespace)

mutation ActivateAndSaveAccount(...) {
  namespace {
    activateAndSaveProfile(...) {
      ...
    }
  }
}

Example: Multiple Serial Mutations (nested in namespace)

mutation ActivateAndSaveAccount(...) {
  namespace {
    activate(...) {
      ...
    }
    saveProfile(...) {
      ...
    }
  }
}

A few considerations:

  • The “saveProfile” operation doesn’t appear to depend on any data returned from “activate”. If it does, we can always call the same API that checks activation status before saving the profile within the save profile resolver.
  • There’s a future requirement to allow profile updates, which means a “saveProfile”-type mutation will need to be exposed anyway.
  • We’re still working out the input/output structure for this functionality. So the absence of that detail here isn’t for simplicity - it’s genuinely still an “unknown.”
  • The Graph I’m working on was originally built as a thin API wrapper during the POC/MVP period - mostly direct mappings of underlying APIs to queries and mutations. I’m now migrating toward a Demand-Oriented Schema Design. That’s why I initially leaned toward a single mutation (it simplifies the consumer experience by abstracting the workflow into one call) rather than requiring consumers to understand and orchestrate multiple nested mutations in a specific order.

Has anyone faced a similar scenario? Which approach did you choose - single mutation or multiple? How did it play out? Were you happy with the decision, or do you wish you’d gone a different route?

As a follow-up, our team decided to move forward with the composite mutation, calling it activateHomeProfile (as opposed to something with ‘and’ in it) and build the underlying resolver / service / datasource logic to be easily re-used when/if we expose individual mutations for each portion of the transaction (for example, later when we need to ‘update home profile’ or if we ever need a stand alone ‘activation’ step).

Hello Tyler!

Good to talk to you again.

A while ago, I was thinking exactly the same way about grouping mutations to reduce the number of requests.

For example, imagine having both createUser and login mutation fields.
Obviously, createUser could return a JWT token, but that would give it an extra responsibility, and conceptually, creating a user should only handle user creation, not authentication.

As you mentioned, mutations are executed sequentially when defined at the root level.
However, when fields are nested, they are executed in parallel.

In my case, I also had a namespace-based structure, and to deal with sequential vs. parallel execution, I had to use aliases for the namespaces.

One interesting behavior I noticed was this:
when the user creation succeeds, the login works fine.
When the createUser field throws an error using throw new GraphQLError, the login field is not called, which is the expected behavior.
However, when using a type-based error handling approach, the login field is always executed, since the previous field (createUser) didn’t actually throw, even though it failed logically.
This happens both when using a success: Boolean! flag or when modeling responses with a union type.

I had tried to create a directive called @dependsOn(requires: "") to reduce the number of requests, but no success.
Check out my idea:

mutation CreateAndLoginMutation(
  $loginInput: LoginInput!
  $createUserInput: CreateUserInput!
) {
  accountsCreate: accounts {
    createUser(input: $createUserInput) {
      __typename
      ... on User {
        id
        name
        email
      }
      ... on EmailIsAlreadyTakenError {
        message
        code
      }
      ... on GenericError {
        message
        code
      }
    }
  }

  accountsLogin: accounts {
    login(input: $loginInput) @dependsOn(requires: "accountsCreate.createUser.__typename === 'User'")  {
      __typename
      ... on Auth {
        token
        user {
          id
          name
          email
        }
      }
      ... on InvalidCredentialsError {
        message
      }
      ... on GenericError {
        message
      }
    }
  }
}

I think I’ll try again, but using Apollo Plugins, @graphql-tools/utils (mapSchema) and metadata, just like I did in the previous topic

I’ve uploaded my custom GraphQL directives (@dependsOn and @range) to GitHub: GitHub - michaeldouglasdev/graphql-directives

They’re working great for most cases, but I’m running into an issue with namespace scenarios where fields depend on nested fields. Ex: accounts { create {...}}

I was digging through the GraphQL-JS code and found executeFieldsSerially (graphql-js/src/execution/execute.ts at 16.x.x · graphql/graphql-js · GitHub). The field results get added to the results reduce function variable, but results is never passed to executeField and consequently not available in buildResolveInfo.

I’m wondering if we could add the results to the rootValue field in the info object. This would make previously executed field results available to subsequent field resolvers, which it inclusive would help a lot with my @dependsOn directive hahaha

Does anyone know if there would be any issues with adding mutation results to rootValue? Or if there’s a better approach for making mutation root field results available to other resolvers?

Maybe I can do a PR.
I’m going to test.

Thanks! :rocket: