GraphQL Mutation Design: Batch Updates
Welcome to another post on GraphQL Mutation Design. In an other post on designing static friendly mutations , we talked about how sometimes accepting plural arguments makes our life simpler:
- It allows for static queries instead of using
nfields to update
nobjects for example.
- It makes “transactions” easier: Our resolver for the mutation can decide to fail all updates if one of them fails. A much harder thing to do if updates are all ran in their own field.
There’s another scenario that is sometimes tricky: Batch operations. Let’s take a different example than a shopping cart this time. Let’s say we’ve got some project board à la Trello or GitHub Projects. Our user may move some cards around, edit a card, a two new cards, and remove one. When they’re done, they click some update button and the board is updated with all the changes (Ok this might not be the best UI, but stay with me :D).
Designing the GraphQL schema to allow this use case can be tricky. If we take some of what was in the first article of this series on Anemic Mutations, we would be tempted to design fine grained mutations for every action possible on a project board:
To enable this use case using that schema, our clients would either need to make
n requests for each change, or to use multiple fields in a single mutation operation:
We’ve talked about what could go wrong here:
- Our mutation is not static friendly
- If any one of those fail, what do you think happens here? Nobody knows, maybe we can look at the docs, maybe they all failed, maybe some of the failed and some of them succeeded.
It’s a bit of a mess and GraphQL itself doesn’t help us here. One option would be to make them all plural, this means we would use a single
removeCards field here instead of using two of them. This helps a bit here, but we still have
We’ve fixed the static problem, but still, if we want to do a batch operation with all these operations, we don’t know what to expect if one of them fails. It’s also a bit awkward to read to result of these mutations. Which state do you trust? For example, should you read everything off the last mutation to get the source of truth? Should you merge all payloads into one? How to handle failures?
Quite a long time ago, I asked a similar question on StackOverflow. Daniel Shafer, one of GraphQL’s co-creators was nice enough to provide me with some hints on this problem. He agreed this was quite a hard problem to solve with many tradeoffs.
He did mention something at the end that sounded really interesting. What if we had a single mutation that accepted a list of operations to make on an object. This would actually solve both problems: our server could reject the whole thing inside a single resolver if an operation fails, and our mutation query string stays the exact same, because operations are provided as variables.
I was reading this answer again recently and it made me think of an RFC opened in 2013, about something called JSON Patch. JSON Path was a proposal to make partial updates to JSON documents using a JSON payload and the PATCH HTTP verb.
Similar to Daniel’s idea no? What if our schema represented these operations? I could imagine a batch mutation looking like this:
Unfortunately, this is not quite possible yet. Union types cannot be used as an input to a field. This has been talked about for a while https://github.com/graphql/graphql-js/issues/207 and is an exciting conversation to follow.
I think this example might help drive forward the idea of unions as an input because of how it allows us to easily write static queries, handle batch operations / transactions within a single field, and a better client experience when it comes to the response.
In the mean time, this is a solution that could also work, although less elegant in my opinion:
As you see, there’s no perfect solution here yet, and each of them have tradeoffs. Thank you for reading ❤️ And if you’ve enjoyed this post, you could follow me on twitter! How have you been designing batch mutations?