Design Pattern: Singleton
The Singleton Pattern is a common design pattern. The main idea is to make sure a class can only have one instance during the whole application, and provide a global way to get this one and only instance.
How to Write the Code
There are a few important points to make a Singleton:
- Private constructor: Stop outside code from creating instances. Only the class itself can create the single instance.
- Global access point: Give a static method for outside code to get the single instance.
- Thread safety: Make sure creating and getting the instance is safe even if multiple threads try at the same time.
Let's take a simple example: database connection manager. This is a good use case for Singleton pattern.
In a web app, there are many business functions that need to read and write the database. If you create a new connection every time, it wastes resources and puts pressure on the app and database.
In this situation, we can use the Singleton pattern to create a global, unique database manager instance. All business code uses this instance to work with the database. This avoids creating extra connections.
There are different ways to write Singleton pattern. The way you write it can change depending on the programming language. More in-depth explanation is in the comments.
1. Eager Initialization
Eager initialization creates the single instance when the class is loaded. This is the simplest way to implement Singleton:
public class DatabaseManager {
// Create a globally unique instance when the class is loaded
// JVM initializes static variables when the class is loaded, ensuring thread safety
private static final DatabaseManager INSTANCE = new DatabaseManager();
private Connection connection;
// Private constructor, external code cannot create an instance
private DatabaseManager() {
// Initialize database connection
this.connection = getConnection("mysql://...", "user", "password");
System.out.println("Connection established.");
}
// Provide a public static global access point
// External code can get the unique instance through this method
public static DatabaseManager getInstance() {
return INSTANCE;
}
// Business methods
public ResultSet execute(String sql) throws SQLException {
...
}
}
class DatabaseManager {
private:
// Create a globally unique instance when the program starts
// C++ static member variables are initialized when the program starts, thread-safe
static DatabaseManager instance;
std::Connection connection;
// Private constructor, external code cannot create an instance
DatabaseManager() {
// Initialize database connection
connection = getConnection("mysql://...", "user", "password");
std::cout << "Connection established." << std::endl;
}
public:
// Delete copy constructor and assignment operator to prevent copying the singleton
DatabaseManager(const DatabaseManager&) = delete;
DatabaseManager& operator=(const DatabaseManager&) = delete;
// Provide a public static global access point
// External code can get the unique instance through this method
static DatabaseManager& getInstance() {
return instance;
}
// Business methods
std::string execute(const std::string& sql) {
// ...
}
};
// Static member definition
DatabaseManager DatabaseManager::instance;
type DatabaseManager struct {
connection *sql.DB
}
var (
// Create a globally unique instance during package initialization
instance *DatabaseManager
)
// Initialize singleton instance when package is loaded
// Go's init() function ensures thread-safe initialization
func init() {
instance = &DatabaseManager{
connection: getConnection("mysql://...", "user", "password"),
}
fmt.Println("Connection established.")
}
// Provide global access point
// External code can get the unique instance through this method
func GetInstance() *DatabaseManager {
return instance
}
// Business methods
func (dm *DatabaseManager) Execute(sql string) string {
// ...
}
class DatabaseManager {
// Create a globally unique instance when the class is loaded
// Static properties in TypeScript/JavaScript are initialized when the class is defined
private static readonly instance: DatabaseManager = new DatabaseManager();
private connection: Connection;
// Private constructor, external code cannot create an instance
private constructor() {
// Initialize database connection
this.connection = getConnection("mysql://...", "user", "password");
console.log("Connection established.");
}
// Provide a public static global access point
// External code can get the unique instance through this method
public static getInstance(): DatabaseManager {
return DatabaseManager.instance;
}
// Business methods
public execute(sql: string): string {
// ...
}
}
class DatabaseManager:
def __init__(self):
# Initialize database connection
self.connection = getConnection("mysql://...", "user", "password")
print("Connection established.")
# Business methods
def execute(self, sql: str) -> str:
# ...
# Create a globally unique instance when the module is loaded
# Python module-level variables are initialized when
# the module is first imported, naturally thread-safe
database_manager = DatabaseManager()
# Just import database_manager when using
2. Lazy Initialization (Double-Checked Locking)
Compared with eager initialization, lazy initialization means the singleton instance will be created only when it is first used.
The advantage of lazy initialization is that if the singleton is not used, no object is created. This saves memory and makes the program start faster. The downside is that extra code is needed to make sure it is thread-safe when using lazy loading.
public class DatabaseManager {
// Use volatile to ensure visibility
private static volatile DatabaseManager instance;
private Connection connection;
private DatabaseManager() {
// Initialize database connection
this.connection = getConnection("mysql://...", "user", "password");
System.out.println("Connection established.");
}
public static DatabaseManager getInstance() {
// First check, return if instance is already created
if (instance == null) {
synchronized (DatabaseManager.class) {
// Second check, only the first thread enters the
// synchronized block will create the instance
if (instance == null) {
instance = new DatabaseManager();
}
}
}
return instance;
}
// Business methods
public ResultSet execute(String sql) throws SQLException {
...
}
}
class DatabaseManager {
private:
std::Connection connection;
DatabaseManager() {
// Initialize database connection
connection = getConnection("mysql://...", "user", "password");
std::cout << "Connection established." << std::endl;
}
public:
// Delete copy constructor and assignment operator
DatabaseManager(const DatabaseManager&) = delete;
DatabaseManager& operator=(const DatabaseManager&) = delete;
static DatabaseManager* getInstance() {
// Since C++11, initialization of static local variables is thread-safe
static DatabaseManager instance;
return &instance;
}
// Business methods
std::string execute(const std::string& sql) {
// ...
}
};
type DatabaseManager struct {
connection *sql.DB
}
var (
instance *DatabaseManager
once sync.Once
)
// Use sync.Once to ensure initialization only once
// sync.Once is the best practice for implementing singleton in Go
func GetInstance() *DatabaseManager {
once.Do(func() {
instance = &DatabaseManager{
connection: getConnection("mysql://...", "user", "password"),
}
fmt.Println("Connection established.")
})
return instance
}
// Business methods
func (dm *DatabaseManager) Execute(sql string) string {
// ...
}
class DatabaseManager {
private static instance: DatabaseManager | null = null;
private connection: Connection;
private constructor() {
// Initialize database connection
this.connection = getConnection("mysql://...", "user", "password");
console.log("Connection established.");
}
public static getInstance(): DatabaseManager {
// JavaScript is single-threaded, no need to consider concurrency
if (DatabaseManager.instance === null) {
DatabaseManager.instance = new DatabaseManager();
}
return DatabaseManager.instance;
}
// Business methods
public execute(sql: string): string {
// ...
}
}
import threading
# Python decorator implementation of lazy loading singleton
def lazy_singleton(cls):
_instance = None
_lock = threading.Lock()
def get_instance(*args, **kwargs):
nonlocal _instance
if _instance is None:
with _lock:
if _instance is None:
_instance = cls(*args, **kwargs)
return _instance
return get_instance
@lazy_singleton
class DatabaseManager:
def __init__(self):
self.connection = getConnection("mysql://...", "user", "password")
print("Connection established.")
# Business methods
def execute(self, sql: str):
# ...
# --- Usage Example ---
# db1 = DatabaseManager()
# db2 = DatabaseManager()
# print(f"Are instances the same? {db1 is db2}") # Output: True
About Thread Safety
The singleton pattern only ensures that concurrent calls to the getInstance
method are thread-safe. It will create and share a single global object correctly.
Other methods (like execute
) are not guaranteed to be thread-safe by the singleton pattern. You need to ensure their safety yourself.
More Use Cases
Configuration Management
Scenario:
When the application starts, it needs to read configuration files (such as database addresses, API keys, etc.). These settings do not change during the app's lifetime and are needed everywhere.
Why use singleton:
Singleton can provide consistent configuration throughout the app and avoids reloading data, which improves performance.
// Pseudocode for usage
public class DatabaseService {
public void connect() {
// Get config from singleton
ConfigManager config = ConfigManager.getInstance();
String dbUrl = config.get("database.url");
String dbUser = config.get("database.user");
// Connect to database...
}
}
public class EmailService {
public void sendEmail() {
// Use the same config object to avoid reloading
ConfigManager config = ConfigManager.getInstance();
String apiKey = config.get("email.api.key");
String smtpHost = config.get("email.smtp.host");
// Send email...
}
}
Logger
Scenario:
In a large application, many modules need to write logs. These logs usually go to the same file or the same log server.
Why use singleton:
If every module creates its own Logger instance and writes to the same file, there will be conflicts, and the logs may be mixed up or lost. A singleton Logger ensures all logging happens through one gateway, and everything is written in order.
Popular log frameworks like Log4j and SLF4J use singleton or similar ideas to keep logs clean and centralized.
public class AppLogger {
private static final AppLogger instance = new AppLogger();
private FileWriter fileWriter;
private AppLogger() {
// All logs go to this one file
fileWriter = new FileWriter("application.log", true);
}
public static AppLogger getInstance() {
return instance;
}
public void log(String level, String message) {
...
}
}
Things to Note
Singleton pattern is powerful and useful, but be careful not to overuse it.
It brings global state, can make the code tightly coupled, and makes unit testing harder. Before you decide to use singleton, make sure the object must really be shared globally.