Sitemap
Press enter or click to view image in full size

Singleton Design Pattern | A comprehensive guide

--

I am writing this blog to reinforce the concept which I learned recently and also to help myself as well as other people to revise at times when we need it.
After the explanations and analogies, a detailed code is at the bottom.

Singleton Pattern

What is singleton pattern?
Singleton pattern is a creational design pattern which is used when only one instance of a class is required across the service. In other words, when a class needs to be instantiated only once throughout an application.

Curiosity question: How about a global variable? Can’t we use that for the same purpose and then use it throughout the application?
Answer: Great! We sure can as long as we are okay with the following things:

  1. Poor encapsulation and security issues: Any object can see and modify its value or re-assign it to a different object without any restrictions or validity checks.
  2. Poor thread safety: Creating thread-safe global variables is tricky and error-prone.
  3. Lack of control over initialisation: Whether we want it, or not, the variable will be initialised. So we do not have the control over initialising it lazily (Lazy loading/initialisation, will discuss it in the blog).
  4. Difficult to Mock and Test: Global variables are very difficult to mock and replace in tests.

Singleton pattern has three main characteristics:

  1. Single Instance: Only one object of a class can exist at any time. If you try to create another instance, you will get a reference to the existing one.
  2. Global Access Point: The instance is accessible throughout the application. We do it by calling a static method of the singleton class, typically named as getInstance()
  3. Lazy or eager/early loading/initialisation: During the time of writing the class, it can be decided whether to load it
    a. When first needed (Lazy loading).
    b. During the class loading time (Eager/early loading).
    The concept of lazy or eager loading will be discussed as we move ahead.

A singleton class is generally made up of only three things:
a. Private constructor: Prevents direct instantiation.
b. Private static variable: Holds the single instance of the class.
c. Public static method: Provides access to the instance.

Code examples:

Eager/Early loading: This term is used for an instance which is loaded during the class loading time and thus whether we use it or not, it will always be loaded and consume memory.

// Class implementing Eager Loading
class SingletonClass {
private static final SingletonClass instance = new SingletonClass();

// private constructor
private SingletonClass() {
// Declaring it private prevents creation of its object using the new keyword
}

// Method to get the instance of class
public static SingletonClass getInstance() {
return instance; // Always returns the same instance
}
}

Eager loading is thread safe since it is assigned during the class loading time, before any thread can access it. So even if multiple threads call getInstance() method, the already loaded instance will be returned.

Lazy loading: This term is used for an instance which loads only when it is first required. Since here we are talking about singleton pattern, so it will only be loaded once, during the first time it is called.
It helps in memory consumption as it doesn’t have to be present even when it is nor needed.

// Class implementing Lazy Loading
class SingletonClass {
// Object declaration
private static SingletonClass instance;

// private constructor
private SingletonClass() {
// Declaring it private prevents creation of its object using the new keyword
}

// Method to get the instance of class
public static SingletonClass getInstance() {
// If the object is not created
if (instance == null) {
// A new object is created
instance = new SingletonClass();
}

// Otherwise the already created object is returned
return instance;
}
}

Lazy loading is not thread safe because it is done during the execution time. A scenario may come when the instance is null initially and multiple different threads call the getInstance() method at the same time.
It will create multiple instances, thus violating the principle behind singleton pattern.

To make lazy loading thread safe, we use 3 different kind of ways:

  • Using Synchronised method:
    This is the simplest way to ensure thread safety. By synchronizing the method which creates instance, we can prevent multiple threads from creating multiple instances.
public class SingletonClass {
// Object declaration
private static SingletonClass instance;

// Private constructor
private SingletonClass() {}

// Synchronized keyword used
public static synchronized SingletonClass getInstance() {
if (instance == null) {
instance = new SingletonClass();
}
return instance;
}
}

This method is thread safe but slow, as every call to getInstance() is synchronised, even after the instance is created.
It hurts the performance due to too many synchronisation checks.

  • Double-Checked locking:
    This is the efficient way of ensuring thread safety. Here we check if the instance is null before we acquire the lock (syncrhonize). Once we acquire the lock, we again check if the instance is null or not.
public class SingletonClass {
// Volatile object declaration
private static volatile SingletonClass instance;

// Private constructor
private SingletonClass() {}

// Thread-safe method using double-checked locking
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if (instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
}

What is the use of volatile keyword?
volatile keyword ensures changes made by one thread are visible to others. Without volatile, one thread might create the Singleton instance, but other threads may not see the updated value due to caching. volatile ensures that the instance is always read from the main memory, so all threads see the most up-to-date version.

Why do we check for null twice?

Thread A: checks instance == null → enters synchronized → creates instance.
Thread B: waits for the lock.
Thread A exits synchronized block.
Thread B now enters synchronized block — but:
→ The first
instance == null check is already passed, and it assumes instance still needs to be created.
→ There is no check inside, so it just creates a new singleton again.

  • Bill Pugh approach:
    This is the most elegant and efficient approach of all.
public class SingletonClass {
// Private constructor
private SingletonClass() {}

// Static inner class to hold the Singleton instance
private static class Holder {
private static final SingletonClass INSTANCE = new SingletonClass();
}

// Public method to return the Singleton instance
public static SingletonClass getInstance() {
return Holder.INSTANCE;
}
}

Here, we simply create a static inner class which holds the instance of singleton class.
In Java, static class loading is thread safe and happens exactly once.
On top of that, inner static classes are not loaded during the class loading time but only when they are first called.

Thus, this provides lazy loading with thread safety and it is the cleanest approach and also is the best practice to implement singleton.

I hope it is clear what singleton pattern is used for and what does it contain. Let’s understand some real-life, real-world analogies as well as practical examples where we can use it:

Get harsh baranwal’s stories in your inbox

Join Medium for free to get updates from this writer.

Real-life example:
President of a country.
— At one time, there is only one president in a country.
— To make the decisions, the system doesn’t create a new one but accesses the existing one.
— If anyone tries to create a new president, the law prevents it.

Real-world example:
Printer spooler in an operating system.
— Imagine multiple applications trying to print documents at the same time.
— You will not want every application to use their own printer handler to send print call to the printer. It will cause chaos.
— A singleton spooler can be used which will ensure that all the print jobs go through the same queue and one after the other.

Practical uses in a tech company:

Logging service in an application:

  • Each class instance logs errors, warnings, event, info, etc.
  • Creating multiple instances of logging class in all the classes would create chaos.
  • Debugging will also become very difficult because of inconsistent flow of execution logged.

In such scenario, it is better to create a singleton logger class which ensures that the problems described do not occur.

Database connection manager:

  • Connection pool explosion. Each repository creates its own connection.
    3 repositories x 20 connections = 60 total connections.
  • Massive resource waste, slower performance.
  • Connection exhaustion. Database might support only 50 connections.

In this case as well, a singleton database connection can reduce the connection from 60 to 3 (1 for each repository). Thus, improving performance and efficiency.

Caching system:

  • Single caching system across multiple classes.
  • Redundant data in multiple caches.
  • Memory waste.
  • Inconsistent data between different objects/services.

This again can be solved with a singleton cache instance giving shared cache across the service, efficient memory use, etc.

Problem without singleton:

// ==========================================
// WITHOUT SINGLETON - Multiple Instances Problem
// ==========================================

class Logger {
private int messageCount = 0; // Tracks number of log messages for this specific logger instance

public Logger() {
// Constructor: logs creation with its unique identity (hashCode)
System.out.println("Logger created: " + this.hashCode());
}

public void log(String message) {
// Increments message count for this instance and prints the message with instance identity
messageCount++;
System.out.println("[Log #" + messageCount + " from " + this.hashCode() + "] " + message);
}

public int getMessageCount() {
// Returns how many messages this instance has logged
return messageCount;
}
}
class ServiceA {
private Logger logger = new Logger(); // ❌ Creates its own Logger instance (separate from others)

public void doWork() {
// Logs two messages using its private Logger instance
logger.log("ServiceA is working");
logger.log("ServiceA completed task");
}

public Logger getLogger() { return logger; } // Allows access to this specific Logger instance
}
class ServiceB {
private Logger logger = new Logger(); // ❌ Another separate Logger instance

public void doWork() {
// Logs two messages with its own Logger
logger.log("ServiceB is processing");
logger.log("ServiceB finished");
}

public Logger getLogger() { return logger; } // Provides access to its logger for inspection
}
class ServiceC {
private Logger logger = new Logger(); // ❌ Yet another new Logger instance

public void doWork() {
// Logs a single message
logger.log("ServiceC is running");
}

public Logger getLogger() { return logger; }
}
// ==========================================
// DEMONSTRATION
// ==========================================
public class SimpleDemo {
public static void main(String[] args) {
System.out.println("=== WITHOUT SINGLETON (Multiple Instances) ===\n");

// Instantiate service objects — each one internally creates a new Logger
ServiceA serviceA = new ServiceA();
ServiceB serviceB = new ServiceB();
ServiceC serviceC = new ServiceC();

System.out.println();

// Each service performs work and logs messages using its own Logger
serviceA.doWork();
serviceB.doWork();
serviceC.doWork();

// Now we inspect the Logger objects used in each service
System.out.println("\n=== PROBLEMS ===");

// Display unique hashCode and message count for each logger
System.out.println("ServiceA Logger: " + serviceA.getLogger().hashCode() +
" (Messages: " + serviceA.getLogger().getMessageCount() + ")");
System.out.println("ServiceB Logger: " + serviceB.getLogger().hashCode() +
" (Messages: " + serviceB.getLogger().getMessageCount() + ")");
System.out.println("ServiceC Logger: " + serviceC.getLogger().hashCode() +
" (Messages: " + serviceC.getLogger().getMessageCount() + ")");

// Explanation of why this is problematic
System.out.println("\n❌ 3 different logger instances created!");
System.out.println("❌ Each has separate message count!");
System.out.println("❌ Total messages scattered across loggers!");
System.out.println("❌ Wasted memory and resources!");
}
}

The output will be:

=== WITHOUT SINGLETON (Multiple Instances) ===

Logger created: 12345678
Logger created: 23456789
Logger created: 34567890

[Log #1 from 12345678] ServiceA is working
[Log #2 from 12345678] ServiceA completed task
[Log #1 from 23456789] ServiceB is processing
[Log #2 from 23456789] ServiceB finished
[Log #1 from 34567890] ServiceC is running

=== PROBLEMS ===
ServiceA Logger: 12345678 (Messages: 2)
ServiceB Logger: 23456789 (Messages: 2)
ServiceC Logger: 34567890 (Messages: 1)

❌ 3 different logger instances created!
❌ Each has separate message count!
❌ Total messages scattered across loggers!
❌ Wasted memory and resources!

Using singleton in the same problem:

/**
* Logger class implemented as a Singleton using Bill Pugh Method
* This is the BEST approach for thread-safe lazy initialization
* Combines lazy loading with JVM's guaranteed thread safety
*/
class Logger {
// Shared message counter across the entire application
private int messageCount = 0;

/**
* Private constructor prevents external instantiation
* This ensures no one can create Logger instances using 'new Logger()'
*/
private Logger() {
System.out.println("✅ Single Logger instance created using Bill Pugh method: " + this.hashCode());
System.out.println("✅ Constructor called by JVM during SingletonHolder class loading!");
System.out.println("✅ Thread-safe by JVM guarantee - no synchronization needed!");
}

/**
* Bill Pugh Singleton Holder Pattern
*
* WHY THIS IS PERFECT:
* 1. LAZY LOADING: SingletonHolder class is only loaded when getInstance() is called
* 2. THREAD SAFE: JVM guarantees that class loading is thread-safe
* 3. NO SYNCHRONIZATION: No performance overhead from synchronized blocks
* 4. NO VOLATILE: No need for volatile keyword
* 5. SIMPLE: Clean and elegant code
*/
private static class SingletonHolder {
// This field is initialized when SingletonHolder class is loaded
// JVM ensures this happens exactly once and is thread-safe
private static final Logger INSTANCE = new Logger();
}

/**
* Thread-safe method to get the single Logger instance
*
* HOW IT WORKS:
* 1. When this method is called, it references SingletonHolder.INSTANCE
* 2. This triggers loading of SingletonHolder class (if not already loaded)
* 3. JVM initializes INSTANCE field during class loading
* 4. JVM guarantees class loading is atomic and thread-safe
* 5. All subsequent calls return the same INSTANCE
*/
public static Logger getInstance() {
// This line triggers SingletonHolder class loading and INSTANCE initialization
return SingletonHolder.INSTANCE; // Always returns the same instance
}

/**
* Log a message using the shared instance
* All messages from all services go to the same counter
*/
public void log(String message) {
messageCount++; // Increments the SHARED counter
System.out.println("[Log #" + messageCount + " from " + this.hashCode() + "] " + message);
}

/**
* Returns the total message count across the entire application
* This gives us centralized statistics!
*/
public int getMessageCount() {
return messageCount;
}

/**
* Get application-wide logging statistics
* Only possible because we have a single shared instance
*/
public void displayStats() {
System.out.println("📊 Total application messages logged: " + messageCount);
System.out.println("📊 Logger instance ID: " + this.hashCode());
}
}
/**
* ServiceA class that uses the shared Singleton Logger
* No longer creates its own Logger instance
*/
class ServiceA {
// Gets the SHARED Logger instance instead of creating a new one
// This will be the same instance used by ServiceB and ServiceC
private Logger logger = Logger.getInstance();

/**
* Performs work and logs messages to the SHARED Logger
* These messages contribute to the application-wide message count
*/
public void doWork() {
logger.log("ServiceA is working"); // Goes to the shared Logger
logger.log("ServiceA completed task"); // Increments shared counter
}

/**
* Returns the shared Logger instance
* Will be identical to what other services return
*/
public Logger getLogger() { return logger; }
}
/**
* ServiceB class that uses the same shared Logger instance
* Notice: No new Logger is created here!
*/
class ServiceB {
// Gets the SAME Logger instance that ServiceA uses
private Logger logger = Logger.getInstance();

/**
* Performs work and logs to the SHARED Logger
* Messages are added to the same centralized log
*/
public void doWork() {
logger.log("ServiceB is processing"); // Goes to shared Logger
logger.log("ServiceB finished"); // Increments shared counter
}

/**
* Returns the same shared Logger instance
*/
public Logger getLogger() { return logger; }
}
/**
* ServiceC class that also uses the shared Logger
* All three services now use the SAME Logger instance
*/
class ServiceC {
// Gets the SAME Logger instance as ServiceA and ServiceB
private Logger logger = Logger.getInstance();

/**
* Performs work with the shared Logger
*/
public void doWork() {
logger.log("ServiceC is running"); // Goes to shared Logger
}

/**
* Returns the same shared Logger instance
*/
public Logger getLogger() { return logger; }
}
// ==========================================
// DEMONSTRATION OF SINGLETON BENEFITS
// ==========================================
public class SingletonDemo {
public static void main(String[] args) {
System.out.println("=== WITH SINGLETON (Single Shared Instance) ===\n");

// Create three services - they will all share the SAME Logger instance
System.out.println("🔧 Creating services...");
ServiceA serviceA = new ServiceA(); // Gets shared Logger instance
ServiceB serviceB = new ServiceB(); // Gets same shared Logger instance
ServiceC serviceC = new ServiceC(); // Gets same shared Logger instance

System.out.println("\n🎯 All services now use the SAME Logger instance!\n");

// Each service uses the SAME shared Logger instance
// All messages go to the same centralized log
serviceA.doWork(); // Logs to shared Logger (messages 1-2)
serviceB.doWork(); // Logs to shared Logger (messages 3-4)
serviceC.doWork(); // Logs to shared Logger (message 5)

// Show the benefits this creates
System.out.println("\n=== SINGLETON BENEFITS ===");

// All services have the SAME Logger instance (same hash code)
System.out.println("ServiceA Logger: " + serviceA.getLogger().hashCode() +
" (Messages: " + serviceA.getLogger().getMessageCount() + ")");
System.out.println("ServiceB Logger: " + serviceB.getLogger().hashCode() +
" (Messages: " + serviceB.getLogger().getMessageCount() + ")");
System.out.println("ServiceC Logger: " + serviceC.getLogger().hashCode() +
" (Messages: " + serviceC.getLogger().getMessageCount() + ")");

// Verify all services use the same instance
System.out.println("\n🔍 Instance Verification:");
System.out.println("ServiceA == ServiceB Logger? " +
(serviceA.getLogger() == serviceB.getLogger()));
System.out.println("ServiceB == ServiceC Logger? " +
(serviceB.getLogger() == serviceC.getLogger()));
System.out.println("ServiceA == ServiceC Logger? " +
(serviceA.getLogger() == serviceC.getLogger()));

// Display centralized statistics
System.out.println("\n📊 Centralized Statistics:");
Logger.getInstance().displayStats();

// Summary of benefits achieved by using Singleton
System.out.println("\n✅ SINGLETON BENEFITS ACHIEVED:");
System.out.println("✅ Only 1 Logger instance created (instead of 3)!");
System.out.println("✅ All services share the same Logger instance!");
System.out.println("✅ Centralized message counting: " +
Logger.getInstance().getMessageCount() + " total messages!");
System.out.println("✅ Memory efficient - no duplicate objects!");
System.out.println("✅ Consistent behavior across all services!");
System.out.println("✅ Easy to configure logging globally!");
System.out.println("✅ Application-wide statistics available!");

// Show what we prevented
System.out.println("\n🚫 PROBLEMS PREVENTED:");
System.out.println("🚫 No multiple Logger instances!");
System.out.println("🚫 No scattered message counts!");
System.out.println("🚫 No wasted memory!");
System.out.println("🚫 No inconsistent logging behavior!");

// Demonstrate thread safety
System.out.println("\n🧵 Testing Thread Safety:");
testThreadSafety();
}

/**
* Demonstrates that Singleton is thread-safe
* Multiple threads will get the same Logger instance
*/
private static void testThreadSafety() {
// Create multiple threads that try to get Logger instance
Runnable task = () -> {
Logger logger = Logger.getInstance();
System.out.println("Thread " + Thread.currentThread().getName() +
" got Logger: " + logger.hashCode());
logger.log("Message from " + Thread.currentThread().getName());
};

// Start 3 threads simultaneously
Thread thread1 = new Thread(task, "Thread-1");
Thread thread2 = new Thread(task, "Thread-2");
Thread thread3 = new Thread(task, "Thread-3");

thread1.start();
thread2.start();
thread3.start();

// Wait for threads to complete
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

System.out.println("\n✅ All threads used the same Logger instance!");
System.out.println("✅ Final message count: " + Logger.getInstance().getMessageCount());
}
}

The output will be:

=== WITH SINGLETON (Single Shared Instance) ===

🔧 Creating services...
✅ Single Logger instance created using Bill Pugh method: 1829164700
✅ Constructor called by JVM during SingletonHolder class loading!
✅ Thread-safe by JVM guarantee - no synchronization needed!

🎯 All services now use the SAME Logger instance!

[Log #1 from 1829164700] ServiceA is working
[Log #2 from 1829164700] ServiceA completed task
[Log #3 from 1829164700] ServiceB is processing
[Log #4 from 1829164700] ServiceB finished
[Log #5 from 1829164700] ServiceC is running

=== SINGLETON BENEFITS ===
ServiceA Logger: 1829164700 (Messages: 5)
ServiceB Logger: 1829164700 (Messages: 5)
ServiceC Logger: 1829164700 (Messages: 5)

🔍 Instance Verification:
ServiceA == ServiceB Logger? true
ServiceB == ServiceC Logger? true
ServiceA == ServiceC Logger? true

📊 Centralized Statistics:
📊 Total application messages logged: 5
📊 Logger instance ID: 1829164700

✅ BILL PUGH SINGLETON BENEFITS:
✅ Only 1 Logger instance created (instead of 3)!
✅ All services share the same Logger instance!
✅ Centralized message counting: 5 total messages!
✅ Memory efficient - no duplicate objects!
✅ Consistent behavior across all services!
✅ Easy to configure logging globally!
✅ Application-wide statistics available!
✅ LAZY LOADING: Instance created only when first needed!
✅ THREAD SAFE: JVM guarantees safety without synchronization!
✅ HIGH PERFORMANCE: No synchronization overhead!
✅ SIMPLE CODE: Clean implementation without complex locking!

🚫 PROBLEMS PREVENTED:
🚫 No multiple Logger instances!
🚫 No scattered message counts!
🚫 No wasted memory!
🚫 No inconsistent logging behavior!
🚫 No synchronization overhead!
🚫 No volatile variables needed!
🚫 No complex double-checked locking!

🧵 Testing Bill Pugh Thread Safety:
Creating 5 threads that simultaneously try to get Logger instance...
Thread Thread-1 got Logger: 1829164700 (No synchronization needed!)
[Log #6 from 1829164700] Message from Thread-1
Thread Thread-2 got Logger: 1829164700 (No synchronization needed!)
[Log #7 from 1829164700] Message from Thread-2
Thread Thread-3 got Logger: 1829164700 (No synchronization needed!)
[Log #8 from 1829164700] Message from Thread-3
Thread Thread-4 got Logger: 1829164700 (No synchronization needed!)
[Log #9 from 1829164700] Message from Thread-4
Thread Thread-5 got Logger: 1829164700 (No synchronization needed!)
[Log #10 from 1829164700] Message from Thread-5

✅ All threads used the same Logger instance!
✅ Bill Pugh method: Thread-safe without synchronization!
✅ JVM handled thread safety during class loading!
✅ Final message count: 10

I hope this gave you a detailed explanation of Singleton Pattern and resolves all your doubts which might occur.

— Harsh Baranwal

--

--