Marcell Ciszek Druzynski
← Back to blog posts

Singleton pattern

The singleton pattern is categorized under creational design patterns. This pattern restricts the instantiation of specific classes, ensuring that only one unique instance of the object can exist. Essentially, it functions as a global variable accessible from any part of the program.

Consider a scenario with a dog as a Singleton. When we refer to this Singleton, we are consistently pointing to the same instance. Any changes made, such as incrementing the dog's age in one module, will reflect universally across all instances where the dog Singleton is employed.

dog.ts
1let instance: Dog;
2class Dog {
3 private name: string;
4 private age: number;
5
6 constructor() {
7 if (instance) {
8 throw new TypeError("Cannot create multiple instances");
9 }
10 this.name = "Bobby";
11 this.age = 1;
12 instance = this;
13 }
14
15 getName(): string {
16 return this.name;
17 }
18 getAge(): number {
19 return this.age;
20 }
21 birthday() {
22 this.age += 1;
23 }
24}
25export default new Dog();
dog.ts
1let instance: Dog;
2class Dog {
3 private name: string;
4 private age: number;
5
6 constructor() {
7 if (instance) {
8 throw new TypeError("Cannot create multiple instances");
9 }
10 this.name = "Bobby";
11 this.age = 1;
12 instance = this;
13 }
14
15 getName(): string {
16 return this.name;
17 }
18 getAge(): number {
19 return this.age;
20 }
21 birthday() {
22 this.age += 1;
23 }
24}
25export default new Dog();

This singleton dog can be globally shared across multiple modules. When modules import the singleton object, they all reference the same instance. A single method, birthday(), is responsible for modifying the age property. Each time any module invokes the birthday() method, the age increases by one, and this change becomes visible throughout all modules utilizing the singleton.

It's important to note that creating a singleton doesn't necessarily require the use of es6 class syntax; a simple object literal suffices and works equally well.

dog.ts
1let age = 1;
2const name = "Bobby";
3const dog = {
4 getAge: () => age,
5 getName: () => name,
6 birthday: () => (age += 1),
7};
8
9export default dog;
dog.ts
1let age = 1;
2const name = "Bobby";
3const dog = {
4 getAge: () => age,
5 getName: () => name,
6 birthday: () => (age += 1),
7};
8
9export default dog;

The concept of a singleton is frequently employed in our applications, often unintentionally. When an object literal is exported from one module, it essentially becomes a singleton. If another module imports this object and makes modifications to its properties, these changes will have a widespread impact throughout the entire program.

1export const person = {
2 name: "Mike",
3 age: 22,
4};
1export const person = {
2 name: "Mike",
3 age: 22,
4};

To safeguard against unintentional property mutations, consider using Object.freeze:

1const person = {
2 name: "Mike",
3 age: 22,
4};
5export default Object.freeze(person);
1const person = {
2 name: "Mike",
3 age: 22,
4};
5export default Object.freeze(person);

Common Use Cases

Why opt for a single instance of an object?

The primary rationale is to regulate access to a shared resource, such as a database or file, to avoid potential issues. Having multiple instances connected to a database, for instance, could lead to chaos. Instead, employing a singleton ensures only one instance exists within our database.

1let instance: DbConnection;
2
3class DbConnection {
4 private uri: string;
5 private isConnected: boolean;
6
7 constructor(uri: string) {
8 if (instance) {
9 throw new Error("Instance already exists");
10 }
11 this.uri = uri;
12 this.isConnected = false;
13 instance = this;
14 }
15
16 connect() {
17 this.isConnected = true;
18 console.log(`DB Connection to ${this.uri}`);
19 }
20
21 disconnect() {
22 this.isConnected = false;
23 console.log(`DB Disconnected from ${this.uri}`);
24 }
25
26 getUri(): string {
27 return this.uri;
28 }
29
30 getIsConnected(): boolean {
31 return this.isConnected;
32 }
33}
34
35export default Object.freeze(
36 new DbConnection("postgres://postgres:postgres@localhost:5432/postgres")
37);
1let instance: DbConnection;
2
3class DbConnection {
4 private uri: string;
5 private isConnected: boolean;
6
7 constructor(uri: string) {
8 if (instance) {
9 throw new Error("Instance already exists");
10 }
11 this.uri = uri;
12 this.isConnected = false;
13 instance = this;
14 }
15
16 connect() {
17 this.isConnected = true;
18 console.log(`DB Connection to ${this.uri}`);
19 }
20
21 disconnect() {
22 this.isConnected = false;
23 console.log(`DB Disconnected from ${this.uri}`);
24 }
25
26 getUri(): string {
27 return this.uri;
28 }
29
30 getIsConnected(): boolean {
31 return this.isConnected;
32 }
33}
34
35export default Object.freeze(
36 new DbConnection("postgres://postgres:postgres@localhost:5432/postgres")
37);

This ensures a single, controlled instance of the DbConnection class with a designated URI for our PostgreSQL database.

Singletons in the Real World

Consider the analogy of a person having only one father. It's an inherent singleton scenario; an individual can't have multiple fathers. From this perspective, a father is essentially a singleton.

Pros vs Cons

Pros

Cons

Summary

References