The Singleton Pattern Explained: Database Connection

The Singleton Pattern Explained: Database Connection

a creational design pattern

Intro

The Singleton design pattern is a creational design pattern you might use when you want to restrict a class from having multiple instances. This pattern is often used when there is a need for a single, centralized resource.

We often use classes to define objects that have certain properties and behaviors. When we create an instance of a class, we are essentially creating a new object that has its own set of attributes and methods.

from animals import Mammal

class Cat(Mammal):
    def __init__(self, name, color, *args, **kwargs):
        self.fur = True
        self.name = name
        self.color = color
    def make_sound(self):
        return "meow"

It's perfectly okay to have multiple instances of the Cat class, and in fact, that's one of the main benefits of object-oriented programming. When we define a class like Cat, we are essentially creating a blueprint that we can use to create multiple instances of that class. Each instance of the class has its own set of attributes and methods and can be treated as a unique object with its own identity.

my_cat = Cat(name="nanaba", color="white")
my_cat.make_sound()  # Output: "meow"

In the real world, there can be many different cats that have different names, ages, and behaviors. By creating multiple instances of the Cat class, we can represent this diversity of cats in our program. We can also modify the attributes and behavior of each instance independently, allowing us to create more complex programs that can handle a variety of situations.

I'm a one-of-a-kind class

However, there are special cases this behavior wouldn't be desirable and we would want our class to have just one instance no matter how many times we try to instantiate a new one. Let's take, for example, a DatabaseConnection class. This is the object we interact with to connect and execute queries to our database.

import psycopg2

class DatabaseConnection:
    def connect(self, host, port, dbname, user, password):
        """
        Connect to the database using the provided credentials.
        """
        try:
            self._connection = psycopg2.connect(
                host=host,
                port=port,
                dbname=dbname,
                user=user,
                password=password
            )
        except Exception as e:
            print(f"Failed to connect to database: {e}")

    def execute(self, query, params=None):
        """
        Execute a SQL query on the database.
        """
        if self._connection is None:
            print("Error: database connection not established")
            return None

        cursor = self._connection.cursor()
        cursor.execute(query, params)
        result = cursor.fetchall()
        cursor.close()

        return result

    def disconnect(self):
        """
        Close the connection to the database.
        """
        if self._connection is not None:
            self._connection.close()
            self._connection = None

Imagine if every user of our app instantiates a new DatabaseConnection class every time they need to interact with the database. When a new instance of the class is created, it would typically involve setting up a network connection, authenticating the user, and allocating other resources like memory and file handles.

Now imagine that we have hundreds or thousands of users accessing the database simultaneously. With so many new connections being established at once, our system would likely become overloaded and slow to respond. Moreover, this approach would consume excessive system resources, making it more likely to experience crashes or other issues.

To avoid these problems, we can use the Singleton pattern to ensure that all requests to the database are handled by a single, shared instance of the DatabaseConnection class. This shared instance can be optimized to manage resources effectively and ensure that all requests to the database are handled efficiently.

Now, make me a single-ton

Let's make our DatabaseConnection a singleton by overriding our class' __new__ method. In Python, __new__ is a special method that gets called when you create a new instance of a class. It's responsible for actually creating the instance and returning it.

import psycopg2

class DatabaseConnection:
    """
    Singleton class for managing database connections.
    """

    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls, *args, **kwargs)
            cls._instance._connection = None
        return cls._instance

In this implementation of the DatabaseConnection class I provided, __new__ is overridden to ensure that only one instance of the class is ever created. The first time the class is instantiated, the _instance attribute is set to None, so __new__ creates a new instance of the class and sets _instance to reference it. Any subsequent attempts to instantiate the class will find that _instance has already been set, and will simply return the existing instance instead of creating a new one.

This ensures that there is only one instance of the DatabaseConnection class in existence at any given time.

Single and lovin' it!

By using a Singleton instance, we can ensure that all database connections are handled consistently, with no conflicts or inconsistencies arising from multiple connections trying to access the same data at the same time. This approach allows us to create a more efficient, reliable, and scalable application, which is especially important in modern, high-traffic web applications where performance and scalability are critical.

Real Implementations

  1. Logging: The Python logging module is implemented as a Singleton so that all modules in an application share a common logging instance.

  2. Database connection management: As discussed in this article, many web frameworks, such as Django, use the Singleton pattern to ensure that only one instance of a database connection exists at any given time.

  3. Configuration management: Configuration data for an application is often stored in a Singleton object so that it can be easily accessed by all parts of the application.

Further reading