The Zurich release has arrived! Interested in new features and functionalities? Click here for more

sachin_namjoshi
Kilo Patron
Kilo Patron

You're looking for advanced, less commonly implemented patterns in ServiceNow GraphQL that can yield significant performance benefits. One such area, often leveraged in other GraphQL ecosystems (like Node.js with Apollo), but less frequently demonstrated or used to its full potential in ServiceNow's specific implementation, is DataLoader-like batching and caching patterns within your script includes.

While ServiceNow's GraphQL framework handles some basic optimizations, the "N+1 query problem" can still arise, especially when fetching related records (dot-walking) where each parent record triggers a separate query for its child records.

The "less used but highly efficient" concept here involves manually implementing a batching and caching mechanism in your Scripted Resolvers to address the N+1 problem, similar to how a DataLoader library would work.

Since ServiceNow's server-side JavaScript doesn't natively expose a DataLoader class, you have to build this pattern yourself.

 

ServiceNow GraphQL Code Example: Manual Batching and Caching in Resolvers

This example demonstrates how you might structure your schema and resolvers to batch sys_user lookups when multiple incidents refer to the same caller, preventing redundant GlideRecord queries.

Scenario: You want to fetch a list of Incidents and their associated Caller's name and email.

Problem: A beginner developer will query for caller would query sys_user for each incident, even if multiple incidents have the same caller, leading to N+1 queries.

Solution: Implement a batching mechanism that collects all unique caller sys_ids across the requested incidents and then performs a single GlideRecord query to fetch all required user data.

 

1. GraphQL Schema Definition (GraphQL Schema record)

 
schema {
  query: Query
}

type Query {
  incidents(limit: Int = 10): [Incident]
}

type Incident {
  sysId: String!
  number: String!
  shortDescription: String
  caller: User # This is where the N+1 potential exists
}

type User {
  sysId: String!
  name: String
  email: String
}

 

2. Script Include - e.g., IncidentGraphQLResolvers

 

This is the core of the efficiency gain. We'll simulate a DataLoader-like pattern.

 

var IncidentGraphQLResolvers = /**  */ (function() {
    function IncidentGraphQLResolvers() {
        // This is where we'll store user IDs to be batched
        this.userIdsToFetch = new Set();
        // This will cache the fetched user data
        this.userCache = {};
    }

    /**
     * Resolver for the 'incidents' query.
     * Fetches incidents and collects caller IDs for batching.
     *  {Object} env - The GraphQL environment object.
     * @returns {Array<GlideRecord>} - Array of incident GlideRecord objects.
     */
    IncidentGraphQLResolvers.prototype.incidentsResolver = function(env) {
        var gr = new GlideRecord('incident');
        gr.addActiveQuery();
        gr.setLimit(env.getArguments().limit || 10);
        gr.query();

        var incidents = [];
        while (gr.next()) {
            incidents.push(gr); // Return GlideRecord object directly for subsequent field resolution
            // Collect caller sys_id for batching
            var callerId = gr.getValue('caller_id');
            if (callerId) {
                this.userIdsToFetch.add(callerId);
            }
        }

        // Before returning incidents, execute the batch fetch for users
        this._batchFetchUsers();

        return incidents;
    };

    /**
     * Resolver for the 'caller' field on the Incident type.
     * Retrieves caller data from the pre-populated cache.
     *  {Object} env - The GraphQL environment object.
     * @returns {Object | null} - User object or null.
     */
    IncidentGraphQLResolvers.prototype.callerResolver = function(env) {
        var incidentGr = env.getSource(); // The parent object is the Incident GlideRecord
        var callerId = incidentGr.getValue('caller_id');

        if (callerId && this.userCache[callerId]) {
            return this.userCache[callerId];
        }
        return null;
    };

    /**
     * Private method to perform the batched GlideRecord query for users.
     * This is called once per top-level query execution.
     */
    IncidentGraphQLResolvers.prototype._batchFetchUsers = function() {
        if (this.userIdsToFetch.size === 0) {
            return;
        }

        var userSysIds = Array.from(this.userIdsToFetch);
        gs.info("Batching user fetch for IDs: " + userSysIds.join(', ')); // Log to see batching in action

        var userGr = new GlideRecord('sys_user');
        userGr.addQuery('sys_id', 'IN', userSysIds.join(','));
        userGr.query();

        while (userGr.next()) {
            this.userCache[userGr.getUniqueValue()] = {
                sysId: userGr.getUniqueValue(),
                name: userGr.name.toString(),
                email: userGr.email.toString()
            };
        }
        // Clear the set for subsequent queries (if this instance were to live longer)
        // In a typical HTTP request context, this Script Include instance is usually short-lived.
        this.userIdsToFetch.clear();
    };

    return IncidentGraphQLResolvers;
})();

// Ensure you define the resolvers mapping in your GraphQL API record.
// For 'incidents' query: IncidentGraphQLResolvers.incidentsResolver
// For 'caller' field on 'Incident' type: IncidentGraphQLResolvers.callerResolver

 

3. How this is a "highly efficient" approach?

 

 

  • Batching: Instead of N individual queries to sys_user for N incidents, this approach performs one GlideRecord query (or a few, depending on the number of unique users) to fetch all required sys_user records in a single go. This dramatically reduces database round trips, which are often the most expensive part of a request.
  • Caching: Once a user record is fetched, it's stored in this.userCache. If the same user appears as a caller for multiple incidents within the same GraphQL request, the resolver retrieves it from the in-memory cache instead of querying the database again.
  • Less Conventional for ServiceNow: While DataLoader is a common pattern in generic GraphQL servers, its explicit, manual implementation within ServiceNow's server-side JavaScript resolvers isn't as widely demonstrated or adopted as simply performing individual GlideRecord lookups. It requires a deeper understanding of the GraphQL execution flow and managing state within the resolver context.

4. When this is useful:

  • When you have nested relationships (Incident -> User, Task -> User, Problem -> User) where the related records are frequently referenced by multiple parent records in a single GraphQL query.
  • For complex UIs that need to display lists of records and their related data.

Important Considerations:

  • State Management: The userIdsToFetch and userCache are instance variables of IncidentGraphQLResolvers. In ServiceNow, a new instance of your Script Include is typically created for each incoming GraphQL request, ensuring that the cache is isolated per request and doesn't leak data between users or concurrent requests.
  • Complexity: This pattern adds complexity compared to a simple GlideRecord.get() in each field resolver. It's a trade-off for performance.
  • Error Handling: Add robust error handling and null checks in a production scenario.
  • Generalization: For a more general solution, you'd abstract this into a reusable "DataLoader" Script Include that can be invoked by different resolvers.

By adopting such patterns, you can significantly optimize your ServiceNow GraphQL APIs for scenarios involving many-to-one or one-to-many relationships, providing a much snappier experience for your client applications.

 

 

Comments
JT8
Tera Contributor

This is awesome thanks @sachin_namjoshi!

ty_roach
Tera Guru

Nice work @sachin_namjoshi .  Could you expand on this and show how this gets called?

Version history
Last update:
‎06-16-2025 10:05 AM
Updated by:
Contributors