Creational Patterns - Encapsulating Object Creation
Singleton, Factory, Builder, Prototype, Object Pool, and DI - a full tour of the creational patterns
Contents
A tour of the five GoF creational patterns that encapsulate the complexity of object creation, plus the modern alternatives of dependency injection and functional composition.
Overview
Creational patterns encapsulate the complexity of creating objects, which keeps the rest of the system independent of what gets created and how. This post covers the five creational patterns defined by the Gang of Four (GoF), together with two modern alternatives: dependency injection (DI) and functional composition. Once creation logic lives in one place, changing the type of object being created no longer forces changes in the calling code.
The Singleton pattern
Definition
A pattern that guarantees a class has exactly one instance and provides a global access point to it.
Where it is used
Singleton shows up wherever an expensive resource is shared or global state has to be managed.
- Database connection pool: connections are expensive to create, so the pool is shared
- Logger: consistent logging across the whole application
- Configuration manager: a single access point for application settings
- Cache manager: sharing a Redis client instance
Implementation example
The example below implements a connection pool as a Singleton. getInstance() uses double-checked locking so it enters the lock only when instance is missing. The volatile field guarantees that the constructed instance is fully visible to other threads.
public class DatabasePool {
private static volatile DatabasePool instance;
private final HikariDataSource dataSource;
private DatabasePool() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost/mydb");
this.dataSource = new HikariDataSource(config);
}
// Double-checked locking (multithread safe)
public static DatabasePool getInstance() {
if (instance == null) {
synchronized (DatabasePool.class) {
if (instance == null) {
instance = new DatabasePool();
}
}
}
return instance;
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}Problems with Singleton
| Problem | Description |
|---|---|
| Hard to test | Global state makes test isolation difficult |
| Hidden dependencies | Reachable from anywhere, so dependencies are hard to trace |
| Multithreading issues | Requires concurrency control (double-checked locking, volatile) |
| SOLID violation | Can violate the Single Responsibility Principle (SRP) by mixing instance management with business logic |
| Tight coupling | Global access produces strong coupling |
The modern alternative: scope management in a DI container
// Spring: singleton scope (the default)
@Component
@Scope("singleton") // the DI container manages the instance
public class DatabasePool {
// no need to implement the Singleton pattern by hand
// easy to replace with a mock in tests
}The Factory Method pattern
Definition
A pattern that defines an interface for creating an object while delegating the choice of concrete class to subclasses.
The problem
// Direct construction: adding a new type requires editing every creation site
Notification notification;
if (type.equals("email")) {
notification = new EmailNotification();
} else if (type.equals("sms")) {
notification = new SmsNotification();
}
// another else if for every new type... -> violates the Open-Closed Principle (OCP)Applying Factory Method
// Define the creation interface
public abstract class NotificationFactory {
public abstract Notification createNotification();
// Can be combined with the template method pattern
public void send(String message) {
Notification notification = createNotification();
notification.setMessage(message);
notification.deliver();
}
}
public class EmailNotificationFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new EmailNotification();
}
}
public class SmsNotificationFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new SmsNotification();
}
}Where it is used
- Creating user-defined objects inside a framework (Spring's
BeanFactory) - Creating a per-provider client when integrating payment gateways
- Per-document-type creators in a document editor
The Abstract Factory pattern
Definition
A pattern that provides an interface for creating families of related objects without naming their concrete classes. It is Factory Method raised one level of abstraction.
Implementation example: a cross-platform UI
// Abstract factory
public interface UIFactory {
Button createButton();
TextField createTextField();
Dialog createDialog();
}
// Concrete factory: Windows platform
public class WindowsUIFactory implements UIFactory {
public Button createButton() { return new WindowsButton(); }
public TextField createTextField() { return new WindowsTextField(); }
public Dialog createDialog() { return new WindowsDialog(); }
}
// Concrete factory: macOS platform
public class MacUIFactory implements UIFactory {
public Button createButton() { return new MacButton(); }
public TextField createTextField() { return new MacTextField(); }
public Dialog createDialog() { return new MacDialog(); }
}
// Client code: does not depend on any concrete factory
public class Application {
private final UIFactory factory;
public Application(UIFactory factory) {
this.factory = factory;
}
public void createUI() {
Button button = factory.createButton();
TextField field = factory.createTextField();
}
}Factory Method versus Abstract Factory
| Aspect | Factory Method | Abstract Factory |
|---|---|---|
| Creation scope | A single product | A product family (several related objects) |
| Level of abstraction | Method level | Object (factory) level |
| How it extends | Add a subclass | Implement a new factory |
| Consistency guarantee | Per product | Across related products |
The Builder pattern
Definition
A pattern that builds a complex object step by step, allowing the same construction procedure to produce different representations.
The problem: the telescoping constructor
// Readability collapses as the parameter count grows
new HttpRequest("GET", "/api/users", null, headers, timeout, retry, cache, auth);Applying Builder
public class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final int timeout;
private final int retryCount;
private final boolean cacheEnabled;
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Map.copyOf(builder.headers); // immutable copy
this.timeout = builder.timeout;
this.retryCount = builder.retryCount;
this.cacheEnabled = builder.cacheEnabled;
}
public static class Builder {
private final String method; // required
private final String url; // required
private Map<String, String> headers = new HashMap<>();
private int timeout = 30000;
private int retryCount = 3;
private boolean cacheEnabled = false;
public Builder(String method, String url) {
this.method = method;
this.url = url;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this; // fluent API
}
public Builder timeout(int ms) {
this.timeout = ms;
return this;
}
public Builder retry(int count) {
this.retryCount = count;
return this;
}
public Builder cache(boolean enabled) {
this.cacheEnabled = enabled;
return this;
}
public HttpRequest build() {
// validation
if (method == null || url == null) {
throw new IllegalStateException("method and url are required");
}
return new HttpRequest(this);
}
}
}
// Usage: a readable fluent API
HttpRequest request = new HttpRequest.Builder("POST", "/api/orders")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token")
.timeout(5000)
.retry(5)
.cache(true)
.build();Advantages of Builder
- Well suited to creating immutable objects
- Clear separation between required and optional parameters
- Better readability through a fluent API
- Combines with Lombok's
@Builderin Java anddata classin Kotlin
The Prototype pattern
Definition
A pattern that creates a new object by cloning an existing one. It pays off when object creation is expensive.
Where it is used
The example clones a document template to produce a new document. clone() performs a deep copy that recurses into nested objects, and the registry looks up a registered prototype by key and returns a copy of it.
public interface DocumentPrototype extends Cloneable {
DocumentPrototype clone();
}
public class SpreadsheetTemplate implements DocumentPrototype {
private List<Sheet> sheets;
private Map<String, Style> styles;
private Configuration config;
// Deep copy
@Override
public SpreadsheetTemplate clone() {
SpreadsheetTemplate copy = new SpreadsheetTemplate();
copy.sheets = this.sheets.stream()
.map(Sheet::clone)
.collect(Collectors.toList());
copy.styles = new HashMap<>(this.styles);
copy.config = this.config.clone();
return copy;
}
}
// Prototype registry
public class TemplateRegistry {
private final Map<String, DocumentPrototype> templates = new HashMap<>();
public void register(String key, DocumentPrototype prototype) {
templates.put(key, prototype);
}
public DocumentPrototype create(String key) {
return templates.get(key).clone();
}
}Deep copy versus shallow copy
| Copy style | Description | Caveats |
|---|---|---|
| Shallow copy | Copies references only | Original and copy share nested objects |
| Deep copy | Recursively copies every nested object | Watch for cycles, and it is expensive |
The Object Pool pattern
Definition
A pattern that creates expensive objects ahead of time and keeps them in a pool, lending and returning them on demand. It is not in the GoF catalog, but it matters a great deal in high-performance systems.
Where it is used
The example pre-creates connections in a pool and lends them out. acquire() takes an available connection and hands it over. release() returns a still-valid connection to the pool and replaces a broken one with a freshly created connection.
public class ConnectionPool {
private final Queue<Connection> available = new ConcurrentLinkedQueue<>();
private final Set<Connection> inUse = ConcurrentHashMap.newKeySet();
private final int maxSize;
public ConnectionPool(int maxSize) {
this.maxSize = maxSize;
// create the initial connections
for (int i = 0; i < maxSize; i++) {
available.add(createConnection());
}
}
public synchronized Connection acquire() {
Connection conn = available.poll();
if (conn != null) {
inUse.add(conn);
return conn;
}
throw new PoolExhaustedException("No available connections");
}
public synchronized void release(Connection conn) {
inUse.remove(conn);
if (conn.isValid()) {
available.offer(conn);
} else {
available.offer(createConnection()); // replace the broken connection
}
}
}Well-known implementations
- HikariCP: the widely used high-performance JDBC connection pool for Java
- gRPC channel pool: connection pooling for gRPC
- Thread pool:
Executors.newFixedThreadPool()and friends
Dependency injection as a pattern
From Factory to DI
Dependency injection moves the responsibility for creating objects from a factory to a container. Where a factory decides "what to create", DI delegates "who creates it" to an external container.
Three forms of DI
// 1. Constructor injection -- the recommended default
public class OrderService {
private final PaymentGateway paymentGateway;
private final InventoryService inventoryService;
public OrderService(PaymentGateway paymentGateway,
InventoryService inventoryService) {
this.paymentGateway = paymentGateway;
this.inventoryService = inventoryService;
}
}
// 2. Setter injection -- optional dependencies
public class ReportService {
private CacheService cache;
public void setCacheService(CacheService cache) {
this.cache = cache;
}
}
// 3. Method injection -- decided at call time
public class NotificationService {
public void send(NotificationChannel channel, String message) {
channel.deliver(message);
}
}The DI container ecosystem (2026)
| Framework | Language | Characteristics |
|---|---|---|
| Spring / Spring Boot | Java, Kotlin | The standard inversion of control (IoC) container, with auto-configuration |
| NestJS | TypeScript | Decorator-based DI, influenced by Angular |
| .NET DI | C# | Built-in DI container |
| Dagger / Hilt | Android (Kotlin) | Compile-time DI, optimized for performance |
| Wire | Go | Based on compile-time code generation |
Functional composition versus inheritance
The modern view: composition over inheritance
As of 2025, the influence of functional programming has made alternatives to inheritance-based creational patterns widespread.
Partial application
// Object creation through partial application instead of a factory
const createLogger = (level: string) => (module: string) => (message: string) => {
console.log(`[${level}][${module}] ${message}`);
};
const errorLogger = createLogger("ERROR");
const authErrorLogger = errorLogger("auth");
authErrorLogger("Login failed"); // [ERROR][auth] Login failedDI through the Reader monad
Functional programming implements dependency injection with the Reader monad:
// Pass dependencies as an environment
type Reader<Env, A> = (env: Env) => A;
interface AppEnv {
db: Database;
logger: Logger;
cache: CacheService;
}
const getUser: Reader<AppEnv, Promise<User>> =
(env) => env.db.query("SELECT * FROM users WHERE id = ?");
const logAndGetUser: Reader<AppEnv, Promise<User>> =
(env) => {
env.logger.info("Fetching user");
return getUser(env);
};Kleisli composition
A pattern for composing functions that return monads, which lets independent functions be assembled in several ways.
// Each function is implemented independently
const validate = (input: RawOrder): Result<ValidOrder> => { /* ... */ };
const enrich = (order: ValidOrder): Result<EnrichedOrder> => { /* ... */ };
const persist = (order: EnrichedOrder): Result<SavedOrder> => { /* ... */ };
// Build the pipeline through Kleisli composition
const processOrder = compose(validate, enrich, persist);Choosing a creational pattern
| Situation | Recommended pattern |
|---|---|
| Exactly one instance is needed | Singleton scope in a DI container (avoid hand-written Singleton) |
| The object type is decided at runtime | Factory Method |
| A family of related objects | Abstract Factory |
| An immutable object with many parameters | Builder |
| Cloning an existing object | Prototype |
| An expensive resource | Object Pool |
| Testability is the priority | Dependency injection |
| Functional style | Partial application, Reader monad |
Summary
Creational patterns separate how an object is built from the code that uses it, so a change in construction does not ripple into the call sites. When only one instance is wanted, the Singleton scope of a DI container beats a hand-written Singleton on both testability and coupling. Immutable objects with many parameters go to Builder, construction whose type is decided at runtime goes to Factory Method, and families of related objects go to Abstract Factory. Expensive resources get reused through an Object Pool, and when testability comes first, dependency injection or functional composition is the better choice.
The next post covers structural patterns.