In this article we will talk about bulk mutation, the desire to update multiple entity with a single input. Some common use case would be if one wants to disable multiple user at the same time or change their permission or whichever use case made lend in this page from the internet.

But first if you are not familiar with mutations, you can get a refresher with Apollo GraphQL 👩‍🚀 mutations. If you’re all set let’s review how you can make a bulk mutations, then check the schema recommendation available.

Multiple mutations in one request

We can use the already existing mutation, in this example to update an entity. GraphQL provides the capability to send multiple mutations at once. If you need to use multiple time the same mutation, you can use aliases (to avoid any duplicate name in the mutation).

mutation(
    $inputOne: UpdateEntityInput!,
    $inputTwo: UpdateEntityInput!,
    $inputThree: UpdateEntityInput!,
    $inputFour: UpdateEntityInput!
) {
    updateOne: updateEntity(input: $inputOne) {
        user { id }
    }
    updateTwo: updateEntity(input: $inputTwo) {
        user { id }
    }
    updateThree: updateEntity(input: $inputThree) {
        user { id }
    }
    updateFour: updateEntity(input: $inputFour) {
        user { id }
    }
}

The alias is prepended to the mutation name like updateOne. For the input as variable, you can have them one after the other in a big json file:

{
  "inputOne": { ... },
  "inputTwo": { ... },
  "inputThree": { ... },
  "inputFour": { ... },
}

The downside of this method, is that you might hit a gateway cap or the http timeout if you stack too many of them. If on the back-end there’s a database call, it might not be the most efficient way to interact with it. In the end, you it will depend on your system’s capability to handle “x many” synchronous mutation at a time.

One mutation on multiple entities

If you have a fair amount of entities you need to modify, and that you can optimize your back-end in a way which makes it faster to deal them at once, then having a synchronous “bulk” mutation could do the trick.

That is useful when all the entity your touching are indexed via a common parent id, or in a way where fetching as a group is actually faster than fetching them individually.

mutation {
    enableEntities(input: EnableEntitiesInput!): EnableEntitiesPayload!
}

type EnableEntitiesInput {
    entityIds: [ID!]!
    parentId: ID!
}

This way you can pass a list of entities from the same parentId and update them. In this case we’ll want to “enable” them, but that’s just for the example. You can then have the payload incorporating errors that should be individually linked to the entity it’s coming from.

type EnableEntitiesPayload {
    enabledEntityIds: [ID!]!
    userErrors: [EnableEntitiesError!]!
}

union EnableEntitiesError = MaxAmountOfEntityReached | InvalidParentId | EnablingError

As describe in our previous mutation article we would have the errors as a union with that are implementing the same UserError interface.

Pagination on a mutation does not make sense, it should only be on queries where you can call the query again to get the next page.

You could send back a MaxAmountOfEntityReached to limit the amount of ids, i.e. entities your bulk mutation can handle in a single synchronous mutation, as you don’t want to hit a gateway timeout.

Bulk asynchronous mutation

The final example would be to use asynchronous mutations. It can either be handled by the service, or via a proxy system that will run the synchronous mutations in a way that don’t break your system. The second one is referred to as bulk mutation.

Architecture

This one is based on the shopify approach where you will build your mutation based on a file with all of your input, similar to what you need to do when sending multiple single mutations in one call. Then it will create the bulk mutation from those data. It’s not just the mutation variables, it’s the type of mutation as well which can use previous mutation output (like a flow, ex: createGroup, createUser, addUserToGroup, addPermission, etc).

sequenceDiagram autonumber participant U as User participant P as Bulk Mutation
Proxy System participant B as Back-end U -->> P: Load data U ->> P: Send Bulk Mutation Note over P: Will use data loaded
to run the mutations Loop For all input in the loaded data P ->> B: createUser P ->> B: addUserToGroup Note over P: Based on previous mutation
to get the created User's ID end U ->> P: Query Bulk Process P ->> U: Give process status

The load data and send bulk mutation, could be done in one step here. But you might have another component of the system for that which the proxy would call. The bulk mutation would use the saved data id to retrieve it. The proxy system will run the mutation for you on the back-end in the background.

The shopify version have more steps to load your data and is a bit less flexible, but it provides subscription that you can subscribe to in order to know when the bulk operation is done.

Schema of an asynchronous mutation

Your asynchronous mutation can be simpler as long as it returns a process id. That’s the key so your system can do the processing while giving the opportunity for the user to check the status.

In the case where you don’t need so much data for your mutation, you can have an asynchronous mutation that will handle the modification of your entities from a simpler input:

type mutation {
    triggerEntityBulkCreation(input: TriggerEntityBulkCreationInput!): TriggerEntityBulkCreationPayload!
}

type query {
    entityBulkCreationProcess(entityBulkCreationProcessId: ID!): EntityBulkCreationProcess
}

This use case would be for creating/modifying multiple resources similarly or following a set logic so that the input doesn’t grow too big. In this case the creation could be done via a set of rule instead of input variables. The TriggerEntityBulkCreationPayload would follow the same principle as before and return an entityBulkCreationProcessId which would then be used in the query to fetch the process.

scalar Timestamp

type EntityBulkCreationProcess {
    id: ID!
    createdAt: Timestamp!
    completedAt: Timestamp
    processErrors: [EntityBulkCreationProcessError!]
    status: String!
}

The process would yield a timestamp for the creation and completion date, with a status to make it more visible if it is completed, completedWithErrors, started, pending, errored, and so on, as string because you may want to keep the flexibility that an enum does not provide for a free text field intended mostly to the user.

The processErrors, would be similar to the UserError that we’ve seen before and could be related to the process itself of the entity or some problem with the input.

You may also have a paginated query to return all the entities you have created, but that’s outside the scope of our bulk mutation 🤷‍♀ So let’s save it for another article!️