AiTechWorlds
AiTechWorlds
University learning companion
Object-Oriented Programming (OOP) · Creational Patterns: Singleton, Factory, Builder
A small product team needs to apply “Creational Patterns: Singleton, Factory, Builder” to a feature that real people depend on. Define the problem, state the assumptions, work through one concrete example, and decide how the team would test the result and recognise failure.
Use this course-level map to place “Creational Patterns: Singleton, Factory, Builder” in the larger system. It complements—not replaces—a topic-specific worked diagram.
McDonald's serves millions of burgers every day across thousands of locations worldwide. Each burger is consistent same ingredients, same assembly order, same result. McDonald's achieves this not by hiring one expert burger maker, but by building a system for creating burgers: the factory.
Separately, every McDonald's location has exactly one point of sale system, one connection to the payment processor, one database. You don't spin up a new database connection for every customer order. There is one, and everyone shares it.
These two ideas a factory for consistent object creation, and a guarantee of exactly one instance are among the most foundational design patterns in software.
In 1994, four software engineers (known as the Gang of Four) published Design Patterns: Elements of Reusable Object Oriented Software. They cataloged 23 recurring solutions to common OOP design problems, organized into three categories:
| Category | Focus | Examples |
|---|---|---|
| Creational | How objects are created | Singleton, Factory, Builder |
| Structural | How objects are assembled | Adapter, Decorator, Facade |
| Behavioral | How objects communicate | Observer, Strategy, Command |
This lesson covers the three most important creational patterns.
The problem: Some resources must exist exactly once. A database connection pool, a configuration manager, a logger. Creating multiple instances would be wasteful or dangerous.
The solution: The Singleton pattern ensures a class has only one instance and provides a global access point to it.
import threading # For thread-safe Singleton
class DatabaseConnection:
_instance = None # Class variable: stores the one instance
_lock = threading.Lock() # Prevents race conditions in multi-threaded apps
def __new__(cls):
# __new__ is called BEFORE __init__ — controls object creation
if cls._instance is None:
with cls._lock: # Acquire lock
if cls._instance is None: # Double-check after lock
cls._instance = super().__new__(cls)
cls._instance.connected = False # Initialize state
return cls._instance # Always return the SAME instance
def connect(self, host: str, database: str):
if not self.connected:
print(f"Connecting to {database} on {host}...")
self.host = host
self.database = database
self.connected = True
else:
print("Already connected — reusing existing connection")
def query(self, sql: str) -> str:
if not self.connected:
raise RuntimeError("Not connected to database")
return f"Result of: {sql}"Proving there is only one instance:
db1 = DatabaseConnection()
db2 = DatabaseConnection()
db1.connect("localhost", "production_db")
# Connecting to production_db on localhost...
db2.connect("remotehost", "other_db")
# Already connected — reusing existing connection
print(db1 is db2) # True — same object in memory
print(id(db1) == id(db2)) # True — identical memory address
print(db2.host) # localhost — db2 IS db1Anti pattern warning: Singleton introduces global state. Use it sparingly for truly shared infrastructure (database connections, config, logging). Never use it to avoid thinking about dependency injection.
The problem: Your code needs to create objects, but the exact type should be determined at runtime based on user input, configuration, or context.
The solution: A factory method encapsulates object creation, returning the right type based on the given parameters.
from abc import ABC, abstractmethod
# Abstract base for all notification types
class Notification(ABC):
@abstractmethod
def send(self, recipient: str, message: str):
pass
# Concrete notification implementations
class EmailNotification(Notification):
def send(self, recipient: str, message: str):
print(f"[EMAIL] To: {recipient} | Message: {message}")
class SMSNotification(Notification):
def send(self, recipient: str, message: str):
# Truncate long SMS messages to 160 characters
short_msg = message[:160]
print(f"[SMS] To: {recipient} | Message: {short_msg}")
class PushNotification(Notification):
def send(self, recipient: str, message: str):
print(f"[PUSH] Device: {recipient} | Alert: {message}")
# The Factory — creates the right notification type
class NotificationFactory:
# Registry maps channel names to their classes
_registry = {
"email": EmailNotification,
"sms": SMSNotification,
"push": PushNotification,
}
@classmethod
def create(cls, channel: str) -> Notification:
# Look up the channel type in the registry
notification_class = cls._registry.get(channel.lower())
if notification_class is None:
available = ", ".join(cls._registry.keys())
raise ValueError(f"Unknown channel '{channel}'. Available: {available}")
return notification_class() # Create and return instance
@classmethod
def register(cls, channel: str, notification_class: type):
# Allow new channels to be added without modifying the factory
cls._registry[channel] = notification_classUsing the factory:
# The calling code doesn't import EmailNotification, SMSNotification, etc.
# It only knows about the factory and the abstract interface.
channels = ["email", "sms", "push"]
for channel in channels:
notifier = NotificationFactory.create(channel)
notifier.send("user@example.com", "Your order has shipped!")Output:
[EMAIL] To: user@example.com | Message: Your order has shipped!
[SMS] To: user@example.com | Message: Your order has shipped!
[PUSH] Device: user@example.com | Alert: Your order has shipped!Adding a new channel (say, Slack) requires zero changes to existing code just register it: NotificationFactory.register("slack", SlackNotification).
The problem: Some objects are complex to construct they have many optional parameters, and the construction order matters. Using a constructor with 12 arguments is confusing and error prone.
The solution: The Builder pattern separates object construction into a series of clear, readable steps.
class SQLQuery:
"""Represents a finished SQL query."""
def __init__(self):
self._table = ""
self._columns = ["*"]
self._conditions = []
self._order_by = None
self._limit = None
def __str__(self) -> str:
# Assemble the SQL string from all the parts
cols = ", ".join(self._columns)
sql = f"SELECT {cols} FROM {self._table}"
if self._conditions:
where_clause = " AND ".join(self._conditions)
sql += f" WHERE {where_clause}"
if self._order_by:
sql += f" ORDER BY {self._order_by}"
if self._limit:
sql += f" LIMIT {self._limit}"
return sql
class SQLQueryBuilder:
"""Builds an SQLQuery step by step."""
def __init__(self):
self._query = SQLQuery() # Start with a blank query
def select(self, *columns: str) -> "SQLQueryBuilder":
# Specify which columns to retrieve
self._query._columns = list(columns)
return self # Return self to enable method chaining
def from_table(self, table: str) -> "SQLQueryBuilder":
# Specify the source table
self._query._table = table
return self
def where(self, condition: str) -> "SQLQueryBuilder":
# Add a WHERE condition (can be called multiple times)
self._query._conditions.append(condition)
return self
def order_by(self, column: str) -> "SQLQueryBuilder":
# Specify sort order
self._query._order_by = column
return self
def limit(self, count: int) -> "SQLQueryBuilder":
# Limit the number of results
self._query._limit = count
return self
def build(self) -> SQLQuery:
# Validate and return the finished query
if not self._query._table:
raise ValueError("Query must specify a table (call .from_table())")
return self._queryUsing the builder:
# Readable, step-by-step construction with method chaining
query = (
SQLQueryBuilder()
.select("id", "name", "email")
.from_table("users")
.where("active = true")
.where("age >= 18")
.order_by("name")
.limit(50)
.build()
)
print(query)Output:
SELECT id, name, email FROM users WHERE active = true AND age >= 18 ORDER BY name LIMIT 50Compare this to a constructor call: SQLQuery("users", ["id","name","email"], ["active = true","age >= 18"], "name", 50) the builder version is far more readable and far less error prone.
| Pattern | Solves | Key Mechanism | Real Use Case |
|---|---|---|---|
| Singleton | Need exactly one instance | Override __new__, cache in class variable | DB connection, config, logger |
| Factory Method | Choose type at runtime | Method returns different subclass instances | Notification channels, payment processors |
| Builder | Complex object construction | Chained methods, deferred .build() call | SQL builders, HTTP clients, test fixtures |
.build().McDonald's doesn't re invent the burger each time. It has a proven system. Design patterns are those proven systems for software.
University learning companion
Object-Oriented Programming (OOP) · Creational Patterns: Singleton, Factory, Builder
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises