Singleton Design Pattern

SINGLETON – Hey, I privately control my own creation to ensure all of you get my single unique copy.

No piracy allowed!

It’s a creational design pattern that ensures that there is only a single instance of the class for everyone to use.

This is useful in managing shared data and configurations across an application instance. It provides a central control to keep the data consistent and reliable for its clients.

Importantly, we must remember that the uniqueness of such instance is confined to its runtime instance. For example, in java each JVM instance can have its separate singleton instance. Hence, in a distributed environment we may need additional implementation to ensure the contents of singleton instances are the same and synchronized.

 

How does the pattern work?

The below diagram shows a java singleton class highlighting its key points.

Firstly, it shows how to ensure a single instance. And, secondly it tells about the strategies to load and update the shared data. The section following it provides more explanation on the same.

A singleton basically has two important things to manage as shown above.

1. Limit Number of Instances to One :

  • A Private Constructor in Java helps us achieve this as it restricts any class other than the Singleton class to create an instance.

2. Load & Manage the Shared Data :

We can choose to load the data at the time of class loading which is called the eager loading. But, the eager loading may seriously impact the class loading time when the data loading is a time consuming process. Hence, the other option is to use is the lazy loading where we load the data at a later point on user’s request. Here, is the summary on these two data loading strategies :

  • Eager Loading Data
    • This is about loading the data at the time of class loading.
    • It’s easy and useful for light data loading.
  • Lazy Loading Data
    • This is about loading data on client request and not during the class loading.
    • This is the recommended approach for heavy and time consuming data loading process.
    • Since we might get overlapping data loading requests, we should make these methods thread safe.

The below are the samples showing the ways to implement the above discussed loading strategies. Finally, the demo client at the end shows how the client APIs are independent of all these loading strategies.

package spectutz.dp.creational.singleton;

import java.util.HashMap;
import java.util.Map;

public class LazyLoadSingleton {
	   private static LazyLoadSingleton mySingletonInstance = new LazyLoadSingleton();
	   private static Map<String,String> sharedData;
	  
	   private LazyLoadSingleton() {}

	   public static synchronized LazyLoadSingleton getInstance() {
	      if(sharedData == null) { //Useful for heavy, time consuming data load 
	    	  loadData();
	      }
	      return mySingletonInstance;
	   }
	   
	   private static void loadData() {
		  //Load the data from its source such as DB or some service 
	      sharedData = new HashMap<String,String>();
	      sharedData.put("myKey01","myData01");
	      sharedData.put("myKey02","myData02");
	   }

	   public synchronized void  updateSharedData(String key, String value) {
		  sharedData.put(key,value);
	   }
	   public String  fetchFromSharedData(String key) {
		   return sharedData.get(key);
	   }
}
package spectutz.dp.creational.singleton;

import java.util.HashMap;
import java.util.Map;

public class EagerLoadSingleton {
	   private static EagerLoadSingleton mySingletonInstance = new EagerLoadSingleton();
	   private static Map<String,String> sharedData;
	  
	   private EagerLoadSingleton() {
		  //Load the data during class loading 
	      sharedData = new HashMap<String,String>();
	      sharedData.put("myKey01","myData01");
	      sharedData.put("myKey02","myData02");	   
	   }

	   public static synchronized EagerLoadSingleton getInstance() {
	      return mySingletonInstance;
	   }
	   
	   public String  fetchFromSharedData(String key) {
		   return sharedData.get(key);
	   }
}
package spectutz.dp.creational.singleton;

import java.util.HashMap;
import java.util.Map;

public class EagerLoadSingleton_StaticBlock {
	   private static EagerLoadSingleton_StaticBlock mySingletonInstance = new EagerLoadSingleton_StaticBlock();
	   private static Map<String,String> sharedData;
	  
	   static {
			  //Load the data during class loading - using static block
		      sharedData = new HashMap<String,String>();
		      sharedData.put("myKey01","myData01");
		      sharedData.put("myKey02","myData02");	   
	   }
	   private EagerLoadSingleton_StaticBlock() {}

	   public static synchronized EagerLoadSingleton_StaticBlock getInstance() {
	      return mySingletonInstance;
	   }
	   
	   public String  fetchFromSharedData(String key) {
		   return sharedData.get(key);
	   }
}
package spectutz.dp.creational.singleton;

public class SingletonDemo {
	
	public static void main(String[] args) {
		//From client point of view -The loading strategies does not make any difference 
		EagerLoadSingleton eagerLoadSingleton = EagerLoadSingleton.getInstance();
		EagerLoadSingleton_StaticBlock eagerLoadSingletonStaticBlock 
										= EagerLoadSingleton_StaticBlock.getInstance();
		
		LazyLoadSingleton lazyLoadSingleton = LazyLoadSingleton.getInstance();
		
		System.out.println(eagerLoadSingleton.fetchFromSharedData("myKey02"));
		System.out.println(eagerLoadSingletonStaticBlock.fetchFromSharedData("myKey02"));
		System.out.println(lazyLoadSingleton.fetchFromSharedData("myKey02"));
	}
	
}
//Demo Output :
/*
myData02
myData02
myData02
*/

 

Conclusion

It’s being a single copy, a singleton provides a centrally controlled shared data management. It prevents duplicate copy and makes the data consistent across its clients.

But, importantly, as the singular copy limit is restricted to a single runtime environment, a distributed environment will have multiple instances. Hence, it may require additional effort to keep the shared data same and synchronized across those instances.