Singleton Pattern in ServiceNow Script Includes

Disclaimer: I used Claude to help turn my notes into this post. The ideas, code, and conclusions are drawn from my own production ServiceNow work.

TL;DR

  • In ServiceNow, a singleton is one instance per transaction, not one instance forever.
  • Use it when setup is expensive and shared state is safe.
  • Avoid it for builder/processor classes that hold per-record mutable state.

What is the Singleton Pattern?

The Singleton pattern ensures a class has only one instance within a given scope. In ServiceNow, that scope is the transaction — a single script execution from start to finish (e.g. one business rule trigger, one scheduled job execution).

1
2
3
4
5
6
7
8
// Singleton
MyUtility.instance = null;
MyUtility.getInstance = function getInstance() {
    if (!MyUtility.instance) {
        MyUtility.instance = new MyUtility();
    }
    return MyUtility.instance;
};

Callers use MyUtility.getInstance() instead of new MyUtility().

Copy/Paste Template (Script Include)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var MyUtility = Class.create();

MyUtility.instance = null;
MyUtility.getInstance = function getInstance() {
    if (!MyUtility.instance) {
        MyUtility.instance = new MyUtility();
    }
    return MyUtility.instance;
};

MyUtility.prototype = {
    initialize: function () {
        this._cache = {};
    },

    doWork: function (key) {
        if (!this._cache[key]) {
            this._cache[key] = "value";
        }
        return this._cache[key];
    },

    type: "MyUtility"
};

Why It Works in ServiceNow

When a Script Include is first loaded in a transaction, ServiceNow evaluates the file and holds the resulting scope in memory for the life of that transaction. The static property MyUtility.instance lives on that cached scope.

  • First call to getInstance() → creates the instance, assigns to MyUtility.instance
  • Second call in the same transaction → scope is still cached, instance is not null → returns the same object
  • Transaction ends → scope released, instance gone
  • New transaction → Script Include re-evaluated → instance = null again

This means the singleton does not persist across transactions. It only prevents multiple instantiations within a single transaction.

Where This Pays Off: High-Fan-Out Transactions

The benefit scales with how much work a single transaction packs in. The more times one transaction reaches for the same helper, the more a shared instance saves. Good candidates:

  • Business rule cascades — one update() fires before/after rules, which trigger more DML, which fire more rules. Ten rules each calling getInstance() share one instance and one round of setup.
  • Scheduled jobs / background scripts — a single execution loops over hundreds of records. Read config or open a connection once, reuse it for every record in the loop.
  • Scripted REST API requests — one inbound call that fans out across several Script Includes gets a single shared config/logger/auth instance for the whole request.
  • Transform maps / import set row processingonBefore/onAfter scripts running across many rows in one import can share a cached lookup instead of re-querying per row.
  • Synchronous Flow actions — script steps in a synchronous flow run in the same transaction and can lean on the same instance.

The flip side is the boundary: work that spins off a new transaction — an async business rule, a scheduled job triggered from your code, an event queued for later — starts a fresh scope and therefore a fresh instance. That’s a feature, not a bug: it keeps unrelated units of work from sharing state.


When to Use It

Good Candidates ✅

Type Reason
Utility/helper classes Stateless methods only, no mutable instance variables
Config readers Read-only, same answer every time
Logger/notifier wrappers One canonical pipeline; you want all callers routing through the same object
Classes with expensive initialize Pay the init cost once per transaction

Bad Candidates ❌

Type Reason
Builder/processor classes Accumulate state per unit of work — shared instance = state bleed between uses
Classes with per-call state Second caller in the same transaction picks up leftover data from the first

The Real Benefit: Avoiding Repeated Expensive Work

The most meaningful reason to use a singleton in ServiceNow is avoiding repeated expensive work — GlideRecord queries, REST/SOAP calls, or any setup that costs real time or counts against transaction limits. That expense shows up in two places: initialize, and the instance methods themselves.

Expensive initialize

If initialize hits the database, a singleton pays that cost once per transaction instead of once per instantiation.

1
2
3
4
5
6
7
// Without singleton — 10 cascading BRs = 10 GlideRecord queries
initialize: function() {
    var gr = new GlideRecord('sys_properties');
    gr.addQuery('name', 'LIKE', 'my_app.');
    gr.query();
    // ...
}

This compounds if initialize instantiates other classes that themselves hit the database. Each new ParentClass() fans out into N child queries — you can hit ServiceNow’s transaction query limits without realizing initialization is the culprit.

Expensive methods (caching across callers)

The benefit doesn’t stop at initialize. Any method that does an expensive GlideRecord lookup or an outbound API call can cache its result on the shared instance, so the second caller in the transaction gets the cached answer for free. A per-new object can’t do this — each instance starts with a cold cache.

1
2
3
4
5
6
7
8
9
getUser: function (sysId) {
    // First caller queries; every caller after reuses the cached record
    if (!this._userCache[sysId]) {
        var gr = new GlideRecord('sys_user');
        gr.get(sysId);
        this._userCache[sysId] = gr;
    }
    return this._userCache[sysId];
}

This is why the template seeds this._cache = {} in initialize — the cache only pays off when every caller shares the same instance.

Cheap work (object literals, in-memory math): Singleton buys you almost nothing. new MyUtility() is essentially free.

Expensive work (DB queries, REST setup, repeated lookups): Singleton is meaningful — one query vs. N queries, whether the cost lives in initialize or in the methods.


The Statefulness Trap

If a singleton holds mutable state, any second caller in the same transaction picks up the previous caller’s leftover data.

1
2
3
4
5
6
// Dangerous if called twice in one transaction
var tracker = WorkItemTracker.getInstance();
tracker.setRecord(someRecord);  // First caller sets this
// ...
// Second caller — gets the SAME instance, someRecord is still set
var tracker2 = WorkItemTracker.getInstance();

The mitigation is an explicit reset method called before use:

1
var trackerRecord = WorkItemTracker.getInstance().startEmpty(); // wipes prior state

If you see a reset(), clear(), or startEmpty() method on a singleton, it’s a signal the author knew the instance could be dirty.


Counter-example: The Builder/Processor Pattern

A class that accumulates state to represent a unit of work should never be a singleton. The instance is the unit of work.

1
2
3
4
// Correct — new instance per CI being processed
new ItemProcessor(inputPayload)
    .buildPayload()
    .submitPayload(target, log);

ItemProcessor builds up this.payload, this.inputData, and this.response per record. A shared instance would mean the second record inherits the first record’s accumulated payload.


Composing the Two: Processors That Lean on Singletons

The builder-vs-singleton split isn’t either/or — it’s how they work together. A per-record processor is exactly the right place to consume a singleton. The processor itself stays a fresh instance per unit of work (so there’s no state bleed), but it pulls its expensive, read-only dependencies from singletons so that cost is paid once per transaction, not once per record.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var ItemProcessor = Class.create();
ItemProcessor.prototype = {
    initialize: function (input) {
        this.input = input;                        // per-record state — lives on the instance
        this.config = ConfigReader.getInstance();  // shared, cached once per transaction
        this.logger = Logger.getInstance();        // one canonical logging pipeline
    },

    buildPayload: function () {
        // getMapping() is a cached lookup on the singleton — read once, reused for every record
        var mapping = this.config.getMapping(this.input.type);
        // ...build this.payload from this.input + mapping
        return this;
    },

    type: "ItemProcessor"
};

Each record gets its own ItemProcessor, so its mutable state is safe — but every processor in the transaction shares the same ConfigReader and Logger. Process ten CIs in one transaction and the config is read once, not ten times.

Rule of thumb: the thing that holds per-record state is created with new; the read-only, expensive-to-build things it depends on are singletons.


Quick Decision Guide

Key: 🔴 Never a singleton  ·  ⚪ No benefit — use new  ·  🟡 Singleton, needs a guard  ·  🟢 Clean singleton

flowchart TD A{"Is the instance itself a unit of work?
(a builder/processor that
accumulates state per record)"} -->|Yes| B(["Never a singleton.
Use new per record —
the instance IS the work."]) A -->|No| C{"Does it do expensive work?
(DB queries or API calls,
in initialize or in its methods)"} C -->|No| D(["Use new directly.
A singleton adds
complexity for no gain."]) C -->|Yes| E{"Does it hold any
mutable state?"} E -->|Yes| F(["Singleton with a guard —
add reset() / startEmpty()
and call it before each use."]) E -->|No| G(["Clean singleton.
Cache freely."]) classDef never fill:#f8d7da,stroke:#c0392b,color:#7a1f1f; classDef neutral fill:#e9ecef,stroke:#868e96,color:#343a40; classDef guard fill:#fff3cd,stroke:#d39e00,color:#7a5b00; classDef good fill:#d4edda,stroke:#28a745,color:#155724; class B never; class D neutral; class F guard; class G good;

Related Posts