Introduction
An Async Singleton is a design pattern that creates a unique class instance, initialized asynchronously, ensuring only one instance exists throughout your application’s lifecycle. I recently had to create an external API client that had these requirements and wanted to share my solution in this post.
The Singleton pattern is particularly useful in situations such as:
- Managing external API connections
- Handling single user/client authentication
- Maintaining session states
- Controlling database connections
Example Scenario
Imagine you’re building a client that needs to authenticate with cookies before making API calls. In this case, you’d want the authentication process to occur only once, and then reuse the same authenticated instance throughout your entire application.
In this guide, you’ll learn:
- How to implement an async singleton in JavaScript
- Techniques for handling asynchronous initialization
- Methods to prevent race conditions
- Real-world applications of this pattern
Let’s get started and learn how to create robust, efficient async singleton class in Javascript
Understanding the Singleton Pattern
The Singleton pattern is a design pattern that restricts a class to having only one instance throughout the entire application lifecycle. Think of it as a global state manager – like having a single remote control for all your smart home devices. Here’s what makes a Singleton:
- A private constructor to prevent direct instantiation
- A private static instance variable
- A public static method that returns the instance
class BasicSingleton {
static instance = null;
constructor(){
if (BasicSingleton.instance) {
throw new Error('Use ApiClient.getInstance() instead');
}
}
static getInstance() {
if (!BasicSingleton.instance) {
BasicSingleton.instance = new BasicSingleton();
}
return BasicSingleton.instance;
}
}
JavaScriptBenefits:
- Memory efficiency – creates only one instance
- Global state management
- Consistent application behavior
Some use cases could be:
- Database connections
- Configurations or loggers
- User authentication states
- Application caches
- Third-party service integrations
The pattern is particularly useful in scenarios where system-wide coordination is crucial, such as logging services or API clients that need to maintain consistent state across different parts of an application.
The Need for Asynchronous Initialization
Traditional singleton patterns work well for synchronous operations, but modern web applications often require data that isn’t immediately available. Consider these scenarios:
- API Authentication: Setting up API clients that need to fetch tokens or establish connections
- User Data Retrieval: Loading user preferences or session information from a remote server
- Database Connections: Establishing connections to databases before allowing operations
- Configuration Loading: Reading configuration files or environment variables from external sources
These challenges require an asynchronous approach to class creation, allowing for non-blocking initialization and proper error handling while maintaining the benefits of the singleton pattern.
Implementing Async Singleton in JavaScript
Let’s dive into creating an async singleton class with a practical example. Here’s a step-by-step implementation that demonstrates how to build a basic async singleton in javascript:
class ApiClient {
static instance = null;
static initializing = false;
constructor() {
if (ApiClient.instance) {
throw new Error('Use ApiClent.getInstance instead of new');
}
}
async #initialize() {
// perform any asynchronous setup tasks here
await this.authenticate();
this.isAuthenticated = true;
}
async authenticate() {
// Simulated async authentication
await new Promise(resolve => setTimeout(resolve, 1000));
this.authToken = 'secret-token';
}
static async getInstance() {
// To avoid race condition. If we're in the middle of initializing we don't want to create another instance
if (ApiClient.initializing) {
await new Promise(resolve => {
const interval = setInterval(() => {
console.log('busy creating other instances at the moment')
if (!ApiClient.initializing) {
clearInterval(interval);
resolve();
}
}, 100);
});
}
if (ApiClient.instance) {
return ApiClient.instance
}
try {
ApiClient.initializing = true;
if (!ApiClient.instance) {
ApiClient.instance = new ApiClient();
await ApiClient.instance.#initialize();
}
return ApiClient.instance;
} finally {
ApiClient.initializing = false;
}
}
}
JavaScriptComponents of the Implementation
This implementation includes several key components:
- Static Properties:
instance
andinitializing
to track the singleton state - Limited Constructor: Prevents direct instantiation using
new
- Static getInstance(): Manages instance creation and caching
- Initialization Method: Handles async setup tasks
- Race condition safe: waits for initialization of other clients ensuring only 1 gets created.
How the Async Singleton Works
The getInstance()
method serves as the heart of our async singleton. It performs a few actions:
- Waits for any other instance to be created before trying to initialize itself
- Returns the instance if it exists
- Sets a lock to let other instances know that it’s initializing
- Creates the new instance asynchronously
- Releases the lock when initialization is finished
Then here’s how to use the class:
async function testClients(){
const instance1promise = ApiClient.getInstance();
const instance2promise = ApiClient.getInstance();
console.log("Created instances")
// this will test that the race condition solution was implemented correctly,
// normally you would just await the call to ApiClient.getInstance()
await Promise.all([instance1promise, instance2promise]).then(([instance1, instance2])=>{
console.log('All done, instance1 === instance2:')
console.log(instance1 === instance2)
})
}
testClients().then()
/** Output **/
//Created instances
//busy creating other instances at the moment
//busy creating other instances at the moment
...
//All done, instance1 === instance2:
//true
JavaScriptManaging Race Conditions
Race conditions pose a slight challenge when implementing this solution. A race condition occurs when multiple parts of your application request an instance while it could be in the middle of being modified or created. In our case this would result in two classes being created, which would be a problem since we only want one instance.
Our code makes use of an initializing field that acts as a lock for other instances.
// static async getInstance(){
//...
if (ApiClient.initializing) {
// waits for initializing to be set to false in the finally block of anothother instance
// even if the other instance fails, we can still attempt to initialize since that code proceeds this check
await new Promise(resolve => {
const interval = setInterval(() => {
console.log('busy creating other instances at the moment')
if (!ApiClient.initializing) {
clearInterval(interval);
resolve();
}
}, 100);
});
}
JavaScriptTo prevent race conditions, we implemented these safeguards:
- Instance Caching: This pattern stores the created instance in a static variable, ensuring that subsequent calls to
getInstance
return the cached instance instead of creating a new one. - Double-Check Locking: This pattern adds a lock mechanism to ensure that only one thread can create the instance at a time. It checks the lock before entering the initialization section where the instance is created.
These patterns ensure that only one instance creation process runs at a time, maintaining the Singleton’s integrity across concurrent requests. The initializing
flag acts as a synchronization mechanism, blocking subsequent initialization attempts while the first one is in progress.
Use Cases for Async Singletons in Web Applications
An asynchronously created singleton class can be used in several real-world web application scenarios:
1. User Authentication Services
This pattern can be used to manage user authentication services effectively. Here are some specific use cases:
- Managing user sessions across multiple components
- Storing and refreshing authentication tokens
- Maintaining consistent user/client state throughout the application
2. API Client Management
This pattern can also be applied to send data and configuration into the constructor. Here’s an example implementation or an external ApiClient:
class APIClient {
static async getInstance() {
if (!this.instance) {
const credentials = await fetchUserCredentials();
this.instance = new APIClient(credentials);
}
return this.instance;
}
}
JavaScriptIn this example, the APIClient
class grabs credentials to create the client and is only created once throughout the applications life cycle.
3. Configuration Management
Another area where async singletons prove valuable is in configuration management. Here are some ways they can be utilized:
- Loading environment or database variables from remote sources
- Caching configuration data to improve performance
These patterns can be valuable in applications where resource management and consistency are a priority. A single source of truth for these services prevents data conflicts and reduces unnecessary API calls or database connections.