What is Singleton?

The Singleton pattern is a design pattern that ensures a class has only one instance and provides a global access point to that instance.

I will keep the post as simple as possible as our brain can process a little at a time (don’t know about yours, mine can’t).

Let’s start with a scenario: Database connection

You want to connect to the database of a service to fetch some information. You have to do the following:

  1. Connect to the database
  2. Read/Write data to the database
  3. Close the connection
  4. Do something with the data or response (i.e. show it to the user)

This is fine when only a few requests hit the service once or twice. But what if there are 100 requests hitting the same resource? You would create 100 database connections. You don’t want that!

Every time you do this, you are creating a new connection to the database. Sometimes, you may want to establish the connection once and maintain it throughout the service’s lifespan. Let’s address this issue using the singleton pattern:

package db

import (
	"context"
	"log"
	"github.com/go-pg/pg/v10"
)

var db *pg.DB
func GetDB() *pg.DB {
	if db != nil{
		return db 
	}

	opts := &pg.Options{
		User: "User",
		Password: "Password",
		Addr: "Address",
		Database: "Database",
	}
	
	db = pg.Connect(opts)	
	
	err := db.Ping(context.Background())
	
	if err != nil {
		log.Println("getDB:", err)
	}
	
	log.Println("connected to database at:",time.Now())
	return db
}

Basically, we define a nullable database object in global context. When we want to use the object, we check whether the object is null or not. If yes, we create the object and return it. Else, fetch the existing object and return the same.

This is a good practical example of a singleton pattern. When you would like to fetch something from the database, you call:

db.GetDB().Query("SELECT * FROM users")

A better approach

You can take advantage of the standard library ‘sync’ to define a singleton for a better approach to define singletons. Here’s how we can do it:

package db

import (
	"context"
	"log"
	"github.com/go-pg/pg/v10"
	"sync"
)

var db *pg.DB
var once sync.Once

func GetDB() (*pg.DB) {
    once.Do(func() {
    
    opts := &pg.Options{
		User: "User",
		Password: "Password",
		Addr: "Address",
		Database: "Database",
	}
	
	db = pg.Connect(opts)	
	
	err := DB.Ping(context.Background())
	
	if err != nil {
		log.Println("getDB:", err)
	}
	
	log.Println("connected to database at:",time.Now())
	
    })
	
	return db
}

Here, whatever function is passed to “sync.Once” is executed only once per run.

Make it concurrent-safe

Golang is known for its prowess in concurrent executions, spawning goroutines with low-memory overhead. What is Golang’s way of making our code concurrent-safe. Let’s take a look:

package db

import (
	"context"
	"log"
	"github.com/go-pg/pg/v10"
	"sync"
)

var db *pg.DB
var once sync.Once
var mu sync.Mutex

func GetDB() (*pg.DB) {
    mu.Lock()
    defer mu.Unlock()
    
    once.Do(func() {
    
    opts := &pg.Options{
		User: "User",
		Password: "Password",
		Addr: "Address",
		Database: "Database",
	}
	
	db = pg.Connect(opts)	
	
	err := DB.Ping(context.Background())
	
	if err != nil {
		log.Println("getDB:", err)
	}
	
	log.Println("connected to database at:",time.Now())
	
    })
	
	return db
}

It is as simple as defining a sync.Mutex variable. We lock the mutex by calling Lock() and Unlock() it when the function is done working with the allocated resources. In this way, we can lock resources and make our functions concurrent-safe.

Here are some places how I benefit from singletons:

  1. Different connections: database, cache, message-queues, otel etc.
  2. Universal logger
  3. App configurations
  4. Custom cache (something I use but not recommended, I make custom caches using singletons)

Caveats

Despite singleton being an amazing pattern in some cases, it has some caveats. Here are some I have faced practically:

  1. Hard to write test specially in TDD setting.
  2. It’s concurrent-safe in Golang, but, you need to do a lot of work in making it concurrent-safe in Python.
  3. Harder to identify dependencies in big codebases.
  4. Dependency injection is a better solution if you are a perfectionist.

Thank you

Thank you guys for reading! I would love to hear your thoughts about my blog posts I have started writing recently. Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!