Design patterns: Creational
In this series (15 parts)
- Introduction to low level design
- SOLID principles
- Design patterns: Creational
- Design patterns: Structural
- Design patterns: Behavioral
- Designing a parking lot
- Designing a library management system
- Designing an elevator system
- Designing a hotel booking system
- Designing a ride-sharing model
- Designing a rate limiter
- Designing a logging framework
- Designing a notification system
- API design and contract-first development
- Data modeling for system design
Object creation sounds trivial. Call new, pass some arguments, done. But in real systems, the “what to create” and “how to configure it” decisions grow complicated fast. You might need to pick a class at runtime based on configuration, enforce that only one instance exists, or assemble an object with dozens of optional fields. Creational patterns are battle-tested answers to these problems.
The SOLID principles told you to depend on abstractions and keep classes focused. Creational patterns show you how to instantiate those abstractions without scattering new ConcreteClass() calls throughout your codebase.
Singleton
A Singleton ensures a class has exactly one instance and provides a global access point to it. Configuration managers, connection pools, and logger registries are classic use cases.
public class ConnectionPool {
private static volatile ConnectionPool instance;
private List<Connection> pool;
private ConnectionPool() {
pool = initializePool();
}
public static ConnectionPool getInstance() {
if (instance == null) {
synchronized (ConnectionPool.class) {
if (instance == null) {
instance = new ConnectionPool();
}
}
}
return instance;
}
}
The double-checked locking above is the standard thread-safe approach in Java. Other languages have simpler idioms (Python’s module-level instance, Go’s sync.Once).
When to avoid Singleton
Singleton is the most overused pattern. The problems:
- Hidden dependencies. Any class can call
getInstance()from anywhere, making the dependency graph invisible. You lose the explicitness that constructor injection provides. - Testing pain. Swapping a Singleton for a mock in tests requires reflection hacks or a reset method, both of which are fragile.
- Concurrency subtlety. Global mutable state plus multiple threads equals race conditions unless you are very careful.
Prefer dependency injection over Singleton when possible. Create the single instance at the composition root and inject it. You get the “one instance” guarantee without the global access problem.
Factory Method
The Factory Method pattern defines an interface for creating objects but lets subclasses decide which class to instantiate. The creator class declares a factory method that returns a product interface. Subclasses override the factory method to return different concrete products.
classDiagram
class Notification {
<<interface>>
+send(String message)
+getChannel() String
}
class EmailNotification {
-String smtpServer
+send(String message)
+getChannel() String
}
class SMSNotification {
-String twilioSid
+send(String message)
+getChannel() String
}
class PushNotification {
-String fcmToken
+send(String message)
+getChannel() String
}
class NotificationFactory {
<<interface>>
+createNotification(User user) Notification
}
class EmailFactory {
+createNotification(User user) Notification
}
class SMSFactory {
+createNotification(User user) Notification
}
class PushFactory {
+createNotification(User user) Notification
}
Notification <|.. EmailNotification
Notification <|.. SMSNotification
Notification <|.. PushNotification
NotificationFactory <|.. EmailFactory
NotificationFactory <|.. SMSFactory
NotificationFactory <|.. PushFactory
EmailFactory ..> EmailNotification : creates
SMSFactory ..> SMSNotification : creates
PushFactory ..> PushNotification : creates
Factory Method for notifications. The caller works with NotificationFactory and Notification interfaces, never touching concrete classes.
The caller asks the factory for a Notification, sends a message through it, and never imports EmailNotification or SMSNotification. This satisfies the Open/Closed Principle: adding a new channel means adding a new factory and a new notification class, with zero changes to existing code.
Real system design use
In a microservice architecture, you often need to create different HTTP clients depending on the target service (some need retries, some need circuit breakers, some need mutual TLS). A factory method lets the framework provide the right client without the business logic knowing the specifics.
Abstract Factory
Abstract Factory extends the factory idea to families of related objects. Instead of one factory method producing one product type, an abstract factory produces a whole set of related products that are designed to work together.
Think of a UI toolkit that supports multiple themes. Each theme needs a consistent set of components: buttons, text fields, checkboxes. Mixing a dark-theme button with a light-theme checkbox would look broken.
interface UIFactory {
Button createButton();
TextField createTextField();
Checkbox createCheckbox();
}
class DarkThemeFactory implements UIFactory {
public Button createButton() { return new DarkButton(); }
public TextField createTextField() { return new DarkTextField(); }
public Checkbox createCheckbox() { return new DarkCheckbox(); }
}
class LightThemeFactory implements UIFactory {
public Button createButton() { return new LightButton(); }
public TextField createTextField() { return new LightTextField(); }
public Checkbox createCheckbox() { return new LightCheckbox(); }
}
The application code receives a UIFactory and calls its methods. Swapping the entire theme means injecting a different factory. No component selection logic lives in the application layer.
When to use Abstract Factory vs Factory Method
Use Factory Method when you need to create one product type with varying implementations. Use Abstract Factory when you need to create a family of products that must be consistent with each other. If your system only has one product dimension, Abstract Factory adds unnecessary complexity.
Builder
Builder separates the construction of a complex object from its representation, allowing the same construction process to produce different configurations. It shines when an object has many optional parameters and constructors would become unwieldy.
classDiagram
class HttpRequest {
-String url
-String method
-Map~String,String~ headers
-String body
-int timeout
-boolean followRedirects
-RetryPolicy retryPolicy
}
class HttpRequestBuilder {
-String url
-String method
-Map~String,String~ headers
-String body
-int timeout
-boolean followRedirects
-RetryPolicy retryPolicy
+url(String) HttpRequestBuilder
+method(String) HttpRequestBuilder
+header(String, String) HttpRequestBuilder
+body(String) HttpRequestBuilder
+timeout(int) HttpRequestBuilder
+followRedirects(boolean) HttpRequestBuilder
+retryPolicy(RetryPolicy) HttpRequestBuilder
+build() HttpRequest
}
HttpRequestBuilder ..> HttpRequest : builds
Builder for HTTP requests. Each setter returns the builder itself, enabling a fluent chain: new HttpRequestBuilder().url("...").method("GET").timeout(5000).build().
Without Builder, you would need a constructor with seven parameters (most optional), or a pile of setter methods that leave the object in an inconsistent state between calls. Builder makes construction explicit and ensures the object is valid when build() is called.
Real system design use
Query builders in ORMs (Hibernate’s CriteriaBuilder, Elasticsearch’s QueryBuilder) are Builders. So are protobuf message constructors, HTTP client configurations, and cloud SDK request objects. Anywhere you see a fluent API with a terminal build() or execute() call, you are looking at the Builder pattern.
Prototype
Prototype creates new objects by cloning an existing instance rather than instantiating from scratch. This is useful when object creation is expensive (heavy initialization, database lookups, complex configuration) and you need many similar instances.
public interface Prototype<T> {
T clone();
}
public class GameUnit implements Prototype<GameUnit> {
private String type;
private int health;
private int attackPower;
private Sprite sprite; // expensive to load
public GameUnit clone() {
GameUnit copy = new GameUnit();
copy.type = this.type;
copy.health = this.health;
copy.attackPower = this.attackPower;
copy.sprite = this.sprite; // shared reference, not reloaded
return copy;
}
}
A prototype registry can store preconfigured templates: “archer,” “knight,” “mage.” Spawning a new unit clones the template and tweaks the specific instance. The sprite (which is costly to load from disk) gets shared rather than reloaded.
Deep vs shallow copy
The critical design decision in Prototype is whether to deep-copy or shallow-copy nested objects. Sharing immutable references (like the sprite above) is safe and efficient. Sharing mutable references creates aliasing bugs. When a cloned object modifies a shared mutable field, the original object sees the change too. Design your prototypes with clear rules about which fields are shared and which are copied.
Real system design use
Document editors use Prototype when users duplicate slides or pages. Configuration management tools clone a base configuration template and override specific fields per environment. JavaScript’s Object.assign and the spread operator are lightweight Prototype implementations baked into the language.
Choosing the right creational pattern
| Problem | Pattern |
|---|---|
| Need exactly one instance globally | Singleton (prefer DI) |
| Need to pick a class at runtime | Factory Method |
| Need families of related objects | Abstract Factory |
| Object has many optional parameters | Builder |
| Cloning is cheaper than creating from scratch | Prototype |
Most real systems combine several. An HTTP client library might use Builder to configure requests, Factory Method to select the transport layer, and Singleton (via DI) for the connection pool. The patterns compose naturally because they solve orthogonal problems.
What comes next
Creational patterns control how objects come into existence. The next article on structural patterns covers how objects compose into larger structures: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy. These patterns are about assembling objects, not creating them.