Skip to content

GraphQL Auth Explosion Case Study, Part 6: GraphQL and Dataloader ​

Series: GraphQL Auth Explosion Case Study

Overview: GraphQL Auth Explosion Case Study

GraphQL Is Where the N+1 Comes Back ​

Everything up to this point can be implemented through REST APIs. Those APIs now accept collections: multiple Account IDs, multiple User account_id join keys, and multiple GroupUser user_id join keys.

GraphQL puts a convenient object graph over those service boundaries. That is useful, but it recreates the abstraction that makes this problem dangerous: field resolvers make remote records look local again. Dataloader is the mechanism that preserves the collection-shaped REST calls underneath that object graph.

It does not do that automatically. A Dataloader source receives a collection of keys; its implementation must actually make a collection-shaped downstream request. A source that loops over its keys is still an N+1.

The Query We Need to Make Cheap ​

This is the organization-wide query used by the demo's wide-organization fixture. One client request asks for every Account, every User belonging to those Accounts, and every Group belonging to those Users:

graphql
query OrganizationUsers($organizationId: ID!, $as: ID!) {
  organization(id: $organizationId, as: $as) {
    id
    name
    accounts {
      id
      name
      users {
        id
        email
        accountId
        groups {
          id
          name
        }
      }
    }
  }
}
json
{
  "organizationId": "<wide-organization fixture ID>",
  "as": "<that organization's top-level administrator ID>"
}

The query looks like a tree because that is the response shape the client wants. It is not the order in which the services can retrieve the data:

The Group lookup is necessarily a second stage. The system does not know which Group records to request until the GroupUser response supplies their IDs.

What Naïve Field Resolution Would Do ​

Without key collection, resolving the nested fields reproduces the cross-network N+1 at each level. The following diagram shows request shape, not authorization traffic; each downstream service still performs the authorization required for the records it returns.

This is why “we use GraphQL” is not a performance strategy. GraphQL supplies a place to express nested work. Without a batching implementation, it becomes a very polite distributed N+1 generator.

What Dataloader Actually Changes ​

When every collection fits within the configured batch size, the same logical field resolutions can become the following downstream data requests. Larger collections are split into ceil(key count / batch size) requests per stage; they must never fall back to one request per record.

The number of GraphQL field resolutions has not changed. The number and shape of network requests has. Dataloader waits until it has the keys requested for a source, calls that source once for the collected keys, and requires the source to return one result for each key in the original order.

Collect, Fetch, Regroup ​

UsersByAccountId demonstrates the essential transformation. Many Account objects independently resolve their users field, but they share one source instance for the same actor and request context:

The regrouping step is part of the contract. Returning one flat collection is not enough: Dataloader must associate each Account key with exactly the Users that belong to it, including an empty array when an Account has none.

GroupsByUserId performs the same operation with a dependency in the middle:

That requires two batched stages, not one Group request. Showing both stages keeps the remaining request cardinality visible: instrumentation should show the GroupUser request followed by the Group request.

MSP Pagination Bounds Each GraphQL Step ​

The mspUserManagement resolver does not discover the full managed set and slice it in User Management. It asks MspManagedOrganization.page for one actor-authorized page from Organization Service. The response contains sorted Account IDs, the complete managed-relationship count, and a continuation offset.

Only those page IDs proceed into the Dataloader sources for Account, User, membership, and Group hydration. Organization Service has already required account.read on every disclosed page Account. Downstream services still enforce their own policies for the records they return; page authorization is not a transferable proof bag.

The GraphQL field does not expose a public pageSize argument. Organization Service applies IAM_DEMO_BATCH_SIZE as its default and maximum for the internal limit parameter; that setting defaults to 1,000 and accepts values from 1 through 10,000. A client continues the walk with the returned cursor.

Dataloader Preserves the Work Done Earlier ​

The Dataloader layer is effective only because the lower layers already expose collection-shaped operations. It can collect Account IDs, but Account Service must accept multiple IDs. It can collect account join keys, but User Service must accept multiple account_id values. It can collect User IDs, but Group Service must accept multiple user_id values.

The implementation must also preserve actor identity, authorization semantics, ordering, empty results, chunk boundaries, and trace context through every source. The focused GraphQL tests cover actor forwarding, pagination state, ordering, and nested data. Those checks establish the described request shape; they do not turn Dataloader itself into an authorization boundary:

Dataloader creates an opportunity to batch. Only the source implementation decides whether that opportunity survives the next network boundary.

The structural result is bounded, collection-shaped transport beneath a nested GraphQL response. Current code and focused tests support that mechanism. Throughput, milliseconds, and the best batch size require measurements collected from the same implementation and authorization semantics.

Next: Architecture Is Throughput