Shopify/graphql-batch: Ruby GraphQL Query Batching
A query batching executor for the graphql gem
At a glance
- What is it?
- GraphQL::Batch adds a loader and promise layer to the graphql gem so Ruby schema fields can combine independent data requests into batch loads. It fits schemas with N+1 access patterns, but requires application-owned loaders and explicit handling of missing records and errors.
- Who is it for?
- Adopt GraphQL::Batch when a graphql-ruby schema performs repeated, groupable data access and can define cache-aware loaders. Do not adopt it for a small schema without a batching problem, or when the application cannot account for per-request loader state, missing keys, and rejection paths.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 88 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why GraphQL::Batch targets Ruby resolver fan-out
GraphQL::Batch solves a specific server-side GraphQL problem: several fields or records can ask for related data during execution, and resolving each request separately can produce repetitive database work. The project supplies an executor for the `graphql` gem rather than a replacement schema, resolver, or database adapter. It is intended for Ruby applications whose resolvers repeatedly load records, counts, associations, or other keyed values.
The unit of batching is a loader. A loader receives grouping arguments through `.for`, accepts individual keys through `.load` or collections through `.load_many`, and receives the collected keys in `perform(ids)`. The README's `RecordLoader` groups by model and then calls `where(id: ids)`, turning multiple key lookups into one model query. This is useful when a response contains many products and each product resolver otherwise asks for the same kind of record.
The library does not decide whether every lookup should be combined. Application code still selects the grouping key, defines the query, and chooses what happens when a key has no result. That boundary is important. GraphQL::Batch can reduce resolver fan-out, but it cannot infer a safe cache identity or correct query strategy for an application's models.
Loader grouping and fulfillment in GraphQL::Batch
Each custom loader subclasses `GraphQL::Batch::Loader`. Its initializer stores grouping arguments, such as the model class used by `RecordLoader`. `perform` receives all keys collected for that loader during the execution batch. Calling `fulfill(record.id, record)` associates each returned record with its key. The documented example also calls `fulfill(id, nil)` for every unfulfilled key.
That second loop is not cosmetic. A loader has to account for keys that the grouped query does not return. Without an explicit `nil` fulfillment, callers may remain unresolved or may receive behavior different from the resolver's intended contract. The README says priming adds a key and value to the loader cache only when the key is already absent, which lets a field seed known values without replacing an existing cache entry.
Grouping arguments are part of correctness. Loaders for different models should not share a loader instance unless they intentionally share behavior. Cache-key overrides are also relevant for composite cases. The `examples/association_loader.rb` exists to address association loading for records with the same database id but different associations, and the example directory includes Active Record, Active Storage, HTTP, and window-key loaders. These examples demonstrate that the abstraction can be adapted to different key identities, but the repository does not present the Active Record examples as required dependencies.
Plugin setup for mutation cache behavior
GraphQL::Batch must be registered as a plugin in the schema. The README specifically places `use GraphQL::Batch` after the mutation declaration so the plugin can extend mutation fields and clear the cache after they resolve. That ordering is a documented integration constraint, not a stylistic preference.
A resolver obtains a loader through `.for` and returns the promise from `.load`. A field resolver therefore does not have to fetch the requested record inline. It describes the key it needs, and the batching executor can collect compatible loader calls before `perform` runs.
The plugin also needs an environment in which the graphql gem integrates with `GraphQL::Batch`. The README links the upstream `graphql-ruby` project, while the loader's promise support comes from `promise.rb`. It does not state a minimum Ruby or graphql gem version in the shown installation and usage sections. The gemspec is the relevant place to verify dependency and runtime compatibility before upgrading the application.
Installing graphql-batch and defining the first loader
Add the gem to the application's `Gemfile`, then install the bundle. The README also gives a direct RubyGems installation command for environments that do not use Bundler.
gem 'graphql-batch'After changing the `Gemfile`, the documented Bundler command reads the bundle and installs the gem dependency.
$ bundleThe direct alternative installs the gem through RubyGems.
$ gem install graphql-batchA first loader can follow the repository's `RecordLoader` pattern. It stores the grouping model, performs one query for all requested ids, fulfills found records, and explicitly fulfills missing ids with `nil`.
class RecordLoader < GraphQL::Batch::Loader
def initialize(model)
@model = model
end
def perform(ids)
@model.where(id: ids).each { |record| fulfill(record.id, record) }
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
endThis code is an application loader, not a loader supplied as a ready-made production component. The examples directory contains additional implementations, but the README does not describe them as a generic drop-in registry.
A first product resolver using load and load_many
Require the library, register the plugin on the schema, and then connect the loader to a field. The following schema excerpt preserves the ordering documented by the README.
require 'graphql/batch'
class MySchema < GraphQL::Schema
query MyQueryType
mutation MyMutationType
use GraphQL::Batch
endA single-product field groups by `Product`, loads one id, and returns the promise. The field definition and resolver are shown together here.
field :product, Types::Product, null: true do
argument :id, ID, required: true
end
def product(id:)
RecordLoader.for(Product).load(id)
endFor a list argument, `load_many(ids)` returns promises for the complete collection. Applications may need to confirm how list ordering is represented because the README example does not state whether the loader sorts results to match the input order.
field :products, [Types::Product, null: true], null: false do
argument :ids, [ID], required: true
end
def products(ids:)
RecordLoader.for(Product).load_many(ids)
endThe direct alternative is to query `Product` inside each resolver. That avoids the batching layer, but it also gives up the shared loader cache, grouped `perform` calls, and the executor state that this project is designed to coordinate.
Promise chains, concurrency, and explicit error fallbacks
The result of `GraphQL::Batch::Loader#load` is a promise backed by the `promise.rb` gem. A resolver can transform a loaded record with `.then`. A second dependent query is also returned from the first callback. In that case, the image lookup cannot begin until the product result supplies `product.image_id`.
def product_image(id:)
RecordLoader.for(Image).load(id).then do |product|
RecordLoader.for(Image).load(product.image_id)
end
endFor independent loads, `Promise.all` lets compatible queries participate in the same batch before `.then` sums their results. This is different from chaining. Chaining encodes a dependency between keys, while `Promise.all` starts a group whose values become useful together.
def all_collections
Promise.all([
CountLoader.for(Shop, :smart_collections).load(context.shop_id),
CountLoader.for(Shop, :custom_collections).load(context.shop_id),
]).then(&:sum)
endA promise can also receive a second lambda for exceptions. The README's fallback rejects the original exception unless it is a `Redis::BaseConnectionError`; in that case it logs and returns a database-backed load. This is an application policy, not automatic error recovery. Loader errors, rejected promises, and fallback availability still need to match the schema's error contract.
Testing, maintenance, and upgrade boundaries
Loaders can be exercised outside a full GraphQL query with `GraphQL::Batch.batch`. According to the README, this sets up thread-local loader state, performs promised batch loads, and clears the state afterward to prevent leakage between tests.
def test_single_query
product = products(:snowboard)
title = GraphQL::Batch.batch do
RecordLoader.for(Product).load(product.id).then(&:title)
end
assert_equal product.title, title
endFor repository development, the documented setup command is `bin/setup`, the test command is `rake test`, and `bin/console` opens an interactive prompt. These commands distinguish contributor workflow from the shorter consumer installation instructions.
The repository is not archived. Its last push was on 2026-07-27, and the most recent listed release is `v0.6.1`, dated 2025-09-24. That is recent enough not to imply abandonment, but release frequency alone does not establish compatibility with every application. The project is released under the MIT License, according to the README and repository license entry. Upgrading can require reviewing loader cache behavior, plugin placement, promise dependencies, and any application-specific key rules rather than changing only a version number.
Editorial conclusion
Adopt GraphQL::Batch when a graphql-ruby schema performs repeated, groupable data access and can define cache-aware loaders. Do not adopt it for a small schema without a batching problem, or when the application cannot account for per-request loader state, missing keys, and rejection paths. Before adoption, verify that the first loader performs one grouped query for each batching key, that every requested key is fulfilled, and that the declared graphql-ruby dependency is compatible with the application.
Frequently asked questions
How do I install GraphQL::Batch in a Ruby application?
Add `gem 'graphql-batch'` to the Gemfile and run `bundle`. The README also documents `gem install graphql-batch` for direct installation. Require it with `require 'graphql/batch'` when integrating it into the application.
What must a GraphQL::Batch loader implement?
A loader subclasses `GraphQL::Batch::Loader`, stores grouping arguments in its initializer, and defines `perform(ids)` to execute the grouped work. It calls `fulfill` for returned and missing keys so requested values can resolve.
When should a GraphQL::Batch resolver use Promise.all?
Use `Promise.all` when several independent promise-producing loads should be grouped and their results are needed together. Use a `.then` chain when one operation depends on the value produced by the preceding load.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/shopify-graphql-batch)Was this useful?