Part of the Design Patterns series. For a high-level overview of software design patterns and principles, see Design Patterns: An Architectural Overview.
Sub-blogs in this series:
Creational Patterns
Creational design patterns abstract the object instantiation process. They make a system independent of how its objects are created, composed, and represented.
As systems evolve from relying on hard-coded class inheritance to relying on object composition, creational patterns shift the focus from hard-coding fixed sets of behaviors to defining flexible, configurable object creation mechanics.
The canonical Gang of Four (GoF) creational patterns are:
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Singleton
1. Factory & Factory Method
A factory is an abstraction—realized as a method, function, or dedicated object—whose sole responsibility is to encapsulate the instantiation and configuration of objects, returning them typed as a common abstraction (an interface or abstract class) so that client code remains decoupled from concrete classes and direct constructor invocations.
Technical Invariants of a Factory
A creational mechanism qualifies as a factory if it enforces these technical properties:
- Encapsulation of Instantiation: It isolates constructor calls (
new ConcreteType()), memory allocation, and complex initialization logic away from the call site. The client never references the concrete implementation's constructor. - Subtype Polymorphism: The declared return type of the creation routine is a supertype (an abstract class or interface). At runtime, the method returns an instance of a compatible subtype that satisfies the Liskov Substitution Principle (LSP).
- Inversion of Concrete Dependencies: It enforces the Dependency Inversion Principle (DIP): high-level modules depend on abstractions (the returned interface), not on concrete details (the specific subclass instantiated).
- Dynamic Selection & Lifecycle Control: The factory retains the authority to determine which subtype to instantiate at runtime (via configuration, parameters, or environment variables) and how to fulfill it (e.g., returning a fresh instance, pulling from an object pool, or returning a cached/flyweight instance).
The Three Technical Forms
Depending on the architectural level, the term "factory" refers to one of three distinct constructs:
- Simple Factory (Static Factory Idiom): A non-polymorphic class or static method containing conditional logic (e.g., a
switchstatement or map registry) that evaluates inputs and returns a concrete subtype disguised as a common supertype. (Example:BorderFactory.createEmptyBorder(),Integer.valueOf(String)) - Factory Method (GoF Creational Pattern): An architectural pattern that relies on inheritance. A creator class declares an abstract or overridable method (
createProduct()), delegating the decision of which concrete subtype to instantiate to derived subclasses. (Example:ILoggerFactory.getLogger(), framework hook methods) - Abstract Factory (GoF Creational Pattern): An architectural pattern that relies on composition. An interface declares a set of distinct factory methods designed to produce an entire suite of related abstract products, guaranteeing cross-product compatibility. (Example:
java.sql.ConnectioncreatingStatement,PreparedStatement, andBlob)
Real-World Example from GitHub: SLF4J (qos-ch/slf4j)
The standard Factory Method pattern is cleanly demonstrated in the ubiquitous SLF4J (Simple Logging Facade for Java) library on GitHub in the qos-ch/slf4j repository.
Unlike the Abstract Factory pattern—which creates families of multiple related objects (like JDBC's Statement, PreparedStatement, and ResultSet)—a normal Factory pattern focuses on decoupling the instantiation of a single product (Logger) from its concrete implementation (Logback, Log4j2, or SimpleLogger).
SLF4J Architecture as a Factory Method
In SLF4J, logging frameworks plug in by providing a concrete implementation of the factory interface ILoggerFactory:
| Factory Role | SLF4J Abstraction | SLF4J Simple Implementation | Logback Implementation (logback-classic) |
|---|---|---|---|
| Product Interface | org.slf4j.Logger | org.slf4j.simple.SimpleLogger | ch.qos.logback.classic.Logger |
| Factory Interface (Creator) | org.slf4j.ILoggerFactory | org.slf4j.simple.SimpleLoggerFactory | ch.qos.logback.classic.LoggerContext |
| Factory Method | getLogger(String name) | Returns SimpleLogger instance | Returns Logback Logger instance |
Source Code Reference
1. The Factory Interface (ILoggerFactory.java)
The interface defines the factory method without knowing which logging framework will fulfill it:
package org.slf4j;
/**
* Defines the Factory Method contract for generating Logger instances.
*/
public interface ILoggerFactory {
/**
* The Factory Method.
* Returns an appropriate Logger instance as specified by the name.
*/
Logger getLogger(String name);
}
2. Concrete Factory (SimpleLoggerFactory.java in slf4j-simple)
The concrete factory implements the factory method, deciding how to instantiate, initialize, and cache concrete SimpleLogger products:
package org.slf4j.simple;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
public class SimpleLoggerFactory implements ILoggerFactory {
private final ConcurrentMap<String, Logger> loggerMap = new ConcurrentHashMap<>();
@Override
public Logger getLogger(String name) {
Logger simpleLogger = loggerMap.get(name);
if (simpleLogger != null) {
return simpleLogger;
}
// Creates the concrete product: SimpleLogger
Logger newInstance = new SimpleLogger(name);
Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
3. Alternative Concrete Factory (LoggerContext.java in Logback)
When Logback is on the classpath, SLF4J binds to Logback's LoggerContext, which implements the same ILoggerFactory to produce complex, multi-appender Logback loggers:
package ch.qos.logback.classic;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
public class LoggerContext implements ILoggerFactory {
@Override
public Logger getLogger(final String name) {
// Creates or retrieves ch.qos.logback.classic.Logger
return getLogger(name, true);
}
}
4. Static Factory Facade (LoggerFactory.java)
To make client code concise, SLF4J provides a static factory facade LoggerFactory. It dynamically discovers the registered ILoggerFactory provider via Java's ServiceLoader (SLF4J 2.x) and delegates creation calls to it:
package org.slf4j;
public final class LoggerFactory {
public static Logger getLogger(Class<?> clazz) {
return getLogger(clazz.getName());
}
public static Logger getLogger(String name) {
// Obtains the active concrete factory (e.g., SimpleLoggerFactory or LoggerContext)
ILoggerFactory iLoggerFactory = getILoggerFactory();
// Delegates product instantiation to the factory method
return iLoggerFactory.getLogger(name);
}
}
Client Application Usage
Client code depends entirely on org.slf4j.Logger and org.slf4j.LoggerFactory. You can swap dependencies in your build configuration (from slf4j-simple to logback-classic or log4j-slf4j-impl) without touching a single line of Java:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PaymentService {
// The factory method resolves the correct Logger implementation transparently
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
public void processPayment(String transactionId, double amount) {
log.info("Processing transaction {} for amount {}", transactionId, amount);
try {
// execute charge logic...
} catch (Exception e) {
log.error("Failed to process transaction {}", transactionId, e);
}
}
}
2. Abstract Factory
An Abstract Factory is an architectural creational pattern that provides an interface for producing families of related or dependent objects without specifying or coupling client code to their concrete classes.
Unlike a standard Factory Method—which encapsulates the instantiation of a single product hierarchy—an Abstract Factory uses object composition to bind together multiple distinct factory methods into a cohesive unit, guaranteeing that the returned products are mutually compatible.
Technical Invariants of an Abstract Factory
An architectural component qualifies as an Abstract Factory if it enforces these properties:
- Family-Level Cohesion (Multi-Product Contract): The factory interface declares two or more distinct creation methods, each returning a completely different abstract product type (e.g.,
createStorage() -> BlobStorage,createQueue() -> MessageQueue). These products are functionally distinct but structurally or environmentally interdependent. - Cross-Product Compatibility Enforcement: The pattern enforces an architectural invariant: products from different families cannot be mixed. Because a concrete factory produces all objects for a given platform, theme, or vendor, client code is structurally prevented from combining incompatible components (such as an AWS storage client with an Azure authentication token, or a PostgreSQL statement with a MySQL connection).
- Composition-Based Client Decoupling: The client does not subclass the factory; it receives a reference to the
AbstractFactoryinterface via dependency injection or composition. The client calls the various creation methods on that single factory instance to obtain its working ecosystem of products. - Two-Dimensional Extensibility Asymmetry: The design exhibits a specific architectural trade-off regarding the Open-Closed Principle (OCP):
- Open for new families: Adding a new vendor, theme, or operating system variant requires only adding a new
ConcreteFactoryclass without modifying existing client code or interfaces. - Closed (rigid) for new products: Introducing an entirely new product type requires modifying the
AbstractFactoryinterface, which cascades breaking changes across all existing concrete factory implementations.
- Open for new families: Adding a new vendor, theme, or operating system variant requires only adding a new
Real-World Example from GitHub: JDBC (java.sql.Connection)
One of the cleanest real-life production examples of the Abstract Factory pattern in Java is the standard Java Database Connectivity (JDBC) API, implemented across production open-source drivers like pgjdbc (PostgreSQL) and mysql-connector-j (MySQL) on GitHub.
In JDBC, java.sql.Connection serves as the Abstract Factory. It does not create just one object; it creates an entire family of database-specific execution products that must work together:
«interface»
java.sql.Connection <--- ABSTRACT FACTORY
/ \
/ \
PgConnection ConnectionImpl <--- CONCRETE FACTORIES
(PostgreSQL) (MySQL)
Mapping Table
| Abstract Factory Role | JDBC Abstraction | PostgreSQL Driver (pgjdbc) | MySQL Driver (mysql-connector-j) |
|---|---|---|---|
| Abstract Factory | java.sql.Connection | org.postgresql.jdbc.PgConnection | com.mysql.cj.jdbc.ConnectionImpl |
| Product A | java.sql.Statement | org.postgresql.jdbc.PgStatement | com.mysql.cj.jdbc.StatementImpl |
| Product B | java.sql.PreparedStatement | org.postgresql.jdbc.PgPreparedStatement | com.mysql.cj.jdbc.ClientPreparedStatement |
| Product C | java.sql.CallableStatement | org.postgresql.jdbc.PgCallableStatement | com.mysql.cj.jdbc.CallableStatement |
| Product D | java.sql.DatabaseMetaData | org.postgresql.jdbc.PgDatabaseMetaData | com.mysql.cj.jdbc.DatabaseMetaData |
| Product E | java.sql.Savepoint | org.postgresql.jdbc.PSQLSavepoint | com.mysql.cj.jdbc.MysqlSavepoint |
A PostgreSQL Statement relies on PostgreSQL wire protocols and cannot interact with an Oracle or MySQL internal connection. The factory guarantees that all created products belong strictly to the same vendor family.
GitHub Source Code Reference: PostgreSQL JDBC Driver
In the official pgjdbc repository, the concrete factory (PgConnection) implements the creation methods defined by Connection:
// Concrete Factory in pgjdbc (org.postgresql.jdbc.PgConnection)
public class PgConnection implements BaseConnection {
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
checkClosed();
// Creates concrete Product A
return new PgStatement(this, resultSetType, resultSetConcurrency);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
checkClosed();
// Creates concrete Product B
return new PgPreparedStatement(this, sql, resultSetType, resultSetConcurrency);
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
checkClosed();
// Creates concrete Product C
return new PgCallableStatement(this, sql, resultSetType, resultSetConcurrency);
}
@Override
public DatabaseMetaData getMetaData() throws SQLException {
checkClosed();
// Creates concrete Product D
return new PgDatabaseMetaData(this);
}
}
How the Client Initializes & Uses the Factory
The client application code interacts solely with the abstract interfaces. It has zero compile-time dependencies on driver-specific classes:
Method A: Java SPI via DriverManager
// Client specifies connection coordinates via URL string
String url = "jdbc:postgresql://localhost:5432/orders_db";
String user = "postgres";
String password = "secretpassword";
// DriverManager acts as the bootstrap loader, returning the Abstract Factory
Connection connectionFactory = DriverManager.getConnection(url, user, password);
Method B: Enterprise Injection via DataSource (e.g., HikariCP)
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
public class DatabaseFactoryProvider {
public static DataSource createDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/orders_db");
config.setUsername("postgres");
config.setPassword("secretpassword");
config.setMaximumPoolSize(10);
return new HikariDataSource(config);
}
}
Client Usage of the Factory Methods
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import javax.sql.DataSource;
public class OrderCheckoutService {
private final DataSource dataSource;
public OrderCheckoutService(DataSource dataSource) {
this.dataSource = dataSource;
}
public void processOrder(int customerId, double orderAmount) {
// 1. Obtain the Abstract Factory instance from the pool
try (Connection conn = dataSource.getConnection()) {
// 2. Request Product A: DatabaseMetaData
DatabaseMetaData meta = conn.getMetaData();
System.out.println("Connected to: " + meta.getDatabaseProductName());
conn.setAutoCommit(false); // Enter manual transaction boundary
// 3. Request Product B: PreparedStatement
String deductFundsSql = "UPDATE accounts SET balance = balance - ? WHERE customer_id = ?";
try (PreparedStatement deductStmt = conn.prepareStatement(deductFundsSql)) {
deductStmt.setDouble(1, orderAmount);
deductStmt.setInt(2, customerId);
deductStmt.executeUpdate();
}
// 4. Request Product C: Savepoint
Savepoint checkpoint = conn.setSavepoint("BalanceDeducted");
try {
String insertAuditSql = "INSERT INTO audit_log (customer_id, event) VALUES (?, ?)";
try (PreparedStatement auditStmt = conn.prepareStatement(insertAuditSql)) {
auditStmt.setInt(1, customerId);
auditStmt.setString(2, "CHECKOUT_PROCESSED");
auditStmt.executeUpdate();
}
} catch (SQLException ex) {
// Roll back only to the checkpoint created by this factory
conn.rollback(checkpoint);
}
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Why is this an Abstract Factory and Not "Just an Interface"?
If Connection were a plain service interface, its methods would look purely operational: conn.connect(); conn.sendBytes(payload);
Now look at what java.sql.Connection actually exposes in the JDK:
public interface Connection {
Statement createStatement(); // Factory Method 1
PreparedStatement prepareStatement(String sql); // Factory Method 2
CallableStatement prepareCall(String sql); // Factory Method 3
Array createArrayOf(String type, Object[] el); // Factory Method 4
Blob createBlob(); // Factory Method 5
Clob createClob(); // Factory Method 6
NClob createNClob(); // Factory Method 7
SQLXML createSQLXML(); // Factory Method 8
Savepoint setSavepoint(); // Factory Method 9
}
Almost every core method on Connection is a constructor in disguise. It instantiates and returns a new object. An object whose role is to manufacture other objects is, by definition, a Factory.
- Without a Factory: Your code would be tightly coupled to vendor implementations:
PgConnection conn = new PgConnection(...);PgStatement stmt = new PgStatement(conn, ...);Migrating to MySQL would require changing every constructor call across the entire codebase. - With Abstract Factory:
Connectionmanufactures every query tool you need, ensuring internal socket protocols, network buffers, character encoders, and type converters match.
Custom Production Pattern: Multi-Cloud Infrastructure Suite
In cloud-native Java projects on GitHub, the pattern is frequently used to avoid cloud-vendor lock-in by generating matching families of cloud resources:
// 1. Abstract Products
public interface BlobStorageService {
void upload(String path, byte[] data);
byte[] download(String path);
}
public interface MessageQueueService {
void publish(String topic, String payload);
String poll(String topic);
}
// 2. Abstract Factory
public interface CloudResourceFactory {
BlobStorageService createStorageService();
MessageQueueService createQueueService();
}
// 3. Concrete Factories (AWS vs. Azure)
public class AwsResourceFactory implements CloudResourceFactory {
private final AwsCredentials credentials;
public AwsResourceFactory(AwsCredentials credentials) {
this.credentials = credentials;
}
@Override
public BlobStorageService createStorageService() {
return new S3StorageService(this.credentials);
}
@Override
public MessageQueueService createQueueService() {
return new SqsQueueService(this.credentials);
}
}
public class AzureResourceFactory implements CloudResourceFactory {
private final AzureConfig config;
public AzureResourceFactory(AzureConfig config) {
this.config = config;
}
@Override
public BlobStorageService createStorageService() {
return new AzureBlobStorageService(this.config);
}
@Override
public MessageQueueService createQueueService() {
return new AzureServiceBusQueue(this.config);
}
}
// 4. Client Integration
public class EventAuditService {
private final BlobStorageService storage;
private final MessageQueueService queue;
public EventAuditService(CloudResourceFactory factory) {
this.storage = factory.createStorageService();
this.queue = factory.createQueueService();
}
public void recordEvent(String eventId, byte[] payload) {
storage.upload("events/" + eventId, payload);
queue.publish("audit-stream", "Stored event: " + eventId);
}
}
When Do You Actually Need an Abstract Factory?
You only pull out an Abstract Factory when you hit the "Multi-Product Compatibility Matrix" problem. All three conditions must be true:
- Multiple distinct products: You aren't just making a Button; you are making a Button, a Checkbox, and a Scrollbar.
- Distinct suites or flavors: You have AWS vs. Azure (or PostgreSQL vs. MySQL, or DarkTheme vs. LightTheme).
- Mixing suites causes a catastrophic or visual failure: If you pass an AWS credential into an Azure storage client, the system crashes. If you render a DarkTheme button with a LightTheme scrollbar, the UI looks broken.
Why Abstract Factories are Rare in Typical App Development:
- Modern Dependency Injection: In Spring Boot / Micronaut, profiles handle suite wiring declaratively without boilerplate factories:
@Configuration @Profile("aws") public class AwsConfig { @Bean public StorageService storage() { return new S3Storage(); } @Bean public QueueService queue() { return new SqsQueue(); } } - The "Rigidity" Penalty (The Open-Closed Trap): Abstract Factories make it easy to add a new family (
GcpFactory), but painful to add a new product (createCacheService()breaks every existing concrete factory and unit test mock).
Factory Method vs. Abstract Factory: Direct Comparison
| Feature | Factory Method | Abstract Factory |
|---|---|---|
| Primary Intent | Decouples the creation of a single product from client code. | Decouples the creation of a suite/family of interdependent products so they are guaranteed to work together. |
| Output | Produces one product type (Logger, Transport, Document). | Produces multiple related product types (Button + Checkbox + Scrollbar). |
| Core Mechanism | Inheritance: Subclasses override a method to specify which concrete class to instantiate. | Composition: The client receives an injected factory object and invokes multiple factory methods on it. |
| Interface Signature | Contains a single creation method: createProduct(). | Contains multiple creation methods: createProductA(), createProductB(). |
| Adding a New Product Family | Not applicable (focuses on single product hierarchies). | Easy: Add a new concrete factory implementing the abstract factory interface without touching existing code. |
| Adding a New Product Type | Easy: Add a new subclass of the product and a corresponding factory variant. | Hard: Adding a new product requires modifying the abstract factory interface and updating every existing concrete factory class. |
| Real-World Examples | ILoggerFactory.getLogger() (SLF4J)<br>BorderFactory.createEmptyBorder() (Swing) | Connection creating Statement, PreparedStatement, and ResultSet (JDBC)<br>Cross-platform UI kits (Windows vs. macOS widgets) |
Quick Decision Framework
Do I need to instantiate an object?
│
├── Is it a standard POJO, DTO, or simple service?
│ └── YES ──> Just use `new MyClass()` or `@Autowired` / `@Inject`.
│
├── Is it ONE swappable implementation behind an interface?
│ └── YES ──> Use Factory Method (or a simple static factory).
│ Example: Logger, PaymentGateway.
│
└── Are there MULTIPLE related products that MUST stay in the same family?
└── YES ──> Use Abstract Factory.
Example: JDBC drivers, document rendering suites, OS widget kits.
3. About Products: Formal GoF Terminology
In the context of software design patterns, a product is the resulting object or component instantiated and returned by a creational pattern, decoupled from the logic used to create it.
A product represents the "what" that gets created, whereas the factory represents the "how."
Formal Gang of Four (GoF) Roles
| Academic Term | What You Call It in Production Code | Real Example |
|---|---|---|
AbstractFactory | Factory, Builder, Provider, Context, Config | Connection, LoggerFactory, SecurityContextProvider |
ConcreteFactory | Driver, Vendor Factory, Implementation Class | PgConnection, SimpleLoggerFactory |
AbstractProduct | Domain Interface: Client, Service, Handler, Renderer | Statement, Logger, HttpClient, PdfRenderer |
ConcreteProduct | The specific underlying class | PgStatement, SimpleLogger, WindowsButton |
Product Family | Ecosystem, Suite, Driver pack, Theme | Postgres JDBC Suite, AWS Provider Suite, Material Theme |
Why "Abstract Product" Instead of "Product Interface"?
- It Can Be an Abstract Class, Not Just an Interface: Products in the same family often share common state (such as an
id, socket buffer, or timestamps) that belongs in an abstract base class. - Historical Context (C++ Predated Java): The GoF published in 1994 using C++ and Smalltalk. C++ achieves abstraction via abstract base classes with pure virtual functions (
virtual void execute() = 0;), having nointerfacekeyword. - Conceptual Role vs. Language Keyword: "Abstract" is an architectural concept meaning incomplete and non-instantiable. Calling it an "Abstract Product" keeps the design pattern valid across languages—whether using Java
interface, Rusttrait, Swiftprotocol, or PythonABC.
4. Builder Pattern
The Builder pattern is an architectural creational pattern that separates the step-by-step construction of a complex object from its final representation, allowing the same construction process to produce different configurations and representations.
While Factory patterns focus on instantiating an object in a single operation—often deciding which subtype to create—the Builder pattern focuses on how a single, complex object is assembled over time through an incremental series of configuration steps.
Technical Invariants
- Step-by-Step Construction (Multi-Phase Lifecycle): Object assembly is broken down into discrete, named operations (
setHost(),withTimeout()). Construction state is accumulated inside the builder until an explicit terminal operation (.build()) is invoked. - Elimination of the Telescoping Constructor Anti-Pattern: Avoids constructors with excessive ambiguous parameters (
new Request(url, null, null, 5000, true, null, false)). - Deferred Atomic Validation & Immutability: The target product can be designed as strictly immutable (all fields
final, no public setters). Cross-field invariants and validation constraints are verified atomically at the moment.build()is executed. - Decoupling Assembly from Representation: The algorithm governing how parts are assembled is decoupled from the internal representation of the parts themselves.
The Two Architectural Forms
1. Modern Idiomatic Builder (Fluent Builder)
The most common variant in modern enterprise programming (Java, Rust, C#, TypeScript). The builder is implemented as a static inner class that uses method chaining:
public final class HttpClient {
private final String baseUrl;
private final int timeoutMs;
private final boolean followRedirects;
// Private constructor: enforce creation strictly via the Builder
private HttpClient(Builder builder) {
this.baseUrl = builder.baseUrl;
this.timeoutMs = builder.timeoutMs;
this.followRedirects = builder.followRedirects;
}
public static class Builder {
private final String baseUrl; // Required
private int timeoutMs = 5000; // Optional default
private boolean followRedirects = true;
public Builder(String baseUrl) {
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("Base URL cannot be empty");
}
this.baseUrl = baseUrl;
}
public Builder timeoutMs(int timeoutMs) {
this.timeoutMs = timeoutMs;
return this;
}
public Builder followRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
return this;
}
public HttpClient build() {
// Atomic validation before instantiation
if (this.timeoutMs < 0) {
throw new IllegalStateException("Timeout cannot be negative");
}
return new HttpClient(this);
}
}
}
2. Classic GoF Builder (With a Director)
The original Gang of Four specification introduced a dedicated Director component responsible for executing the steps in a fixed sequence:
[ Client ] ──> [ Director ] ──> [ «interface» Builder ]
▲
┌────────────────┴────────────────┐
│ │
[ ConcreteBuilderA ] [ ConcreteBuilderB ]
│ │
▼ ▼
ProductA ProductB
Builder(Interface): Declares abstract assembly steps (buildHeader(),buildBody(),buildFooter()).ConcreteBuilder: Maintains product state and implements the assembly steps for a specific format (PdfDocumentBuildervs.HtmlDocumentBuilder).Director: Accepts aBuilderinstance and dictates the exact algorithm or sequence of calls (director.constructInvoice()).Product: The resulting complex object retrieved viabuilder.getResult().
Comparison: Factory vs. Abstract Factory vs. Builder
| Dimension | Factory / Factory Method | Abstract Factory | Builder |
|---|---|---|---|
| Creation Style | Single-shot method invocation | Single-shot method invocations per family | Multi-step incremental assembly |
| Primary Focus | What concrete subtype to instantiate | Which ecosystem/suite of products to instantiate | How to assemble a complex object with many options |
| Product Structure | Often simple or uniform | A family of independent components | Typically a single, deeply configured or composite object |
| Immutability Handling | Requires all parameters at once in the constructor | Requires all parameters per product constructor | Collects parameters iteratively, then yields an immutable object |
5. Prototype Pattern
The Prototype pattern is a creational design pattern where new objects are produced by cloning an existing instance (the prototype) rather than creating new instances from scratch through constructors or factories.
Core Technical Invariants of the Prototype Pattern
- Delegated Self-Duplication: The object itself handles its own replication via a method like
clone()orcopy(). This allows a client to duplicate an object without knowing its concrete class or private internal fields. - Bypassing Expensive Construction: Instead of repeating expensive computations, network fetches, or database queries to construct an object, the system initializes one baseline prototype and clones it whenever an additional instance is needed.
- Decoupling from Concrete Types: The client interacts strictly with an abstract interface declaring the duplication method:
prototype.clone() -> Prototype
The Java Problem: Why java.lang.Cloneable Is Broken
Java attempted to build native support for the Prototype pattern into its standard library, but the implementation introduced major design flaws that Joshua Bloch famously detailed in Effective Java:
// How the JDK attempted it:
public class BadExample implements Cloneable {
private String name;
private List<String> permissions;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone(); // Native shallow field-by-field copy
}
}
The native Java mechanism creates four distinct problems:
- Interface Contract Violation:
java.lang.Cloneableis a marker interface containing zero methods. The actualclone()method is defined asprotectedonjava.lang.Object. ImplementingCloneablesimply flips an internal JVM flag to allowsuper.clone()to execute without throwing aCloneNotSupportedException. - Bypasses Constructors:
super.clone()creates an object in memory via low-level JVM memory allocation without calling any constructor. This circumvents constructor-based invariants, validations, and security checks. - Shallow Copy by Default:
super.clone()only performs a shallow bitwise copy of object references. In the example above, both the clone and original share the exact samepermissionslist reference. Mutating the list in the clone silently corrupts the original object. - Final Fields Conflict: If you make fields
finalto ensure immutability,clone()cannot reassign deep-copied versions of those fields.
How to Implement Prototype in Modern Java
In production Java, the Prototype pattern is implemented in one of three idiomatic ways:
1. Custom Prototype Interface (Pure GoF Pattern)
Define a generic, type-safe interface that explicitly handles deep cloning without relying on Object.clone():
// 1. Prototype Interface
public interface Prototype<T> {
T copy();
}
// 2. Concrete Prototype
public class ServerConfiguration implements Prototype<ServerConfiguration> {
private String host;
private int port;
private Map<String, String> headers;
public ServerConfiguration(String host, int port, Map<String, String> headers) {
this.host = host;
this.port = port;
this.headers = new HashMap<>(headers);
}
@Override
public ServerConfiguration copy() {
// Enforce true deep-copying and valid construction
Map<String, String> clonedHeaders = new HashMap<>(this.headers);
return new ServerConfiguration(this.host, this.port, clonedHeaders);
}
public void setPort(int port) { this.port = port; }
}
// 3. Client Usage
ServerConfiguration baseline = new ServerConfiguration("10.0.0.1", 8080, Map.of("Auth", "Bearer-xyz"));
// Clone the prototype and modify only what varies
ServerConfiguration workerNode = baseline.copy();
workerNode.setPort(8081);
2. Copy Constructors / Copy Factories (The Effective Java Standard)
This is the most common industry replacement for Cloneable. The class provides a constructor or static factory method that accepts an instance of itself:
public class UserSession {
private final String userId;
private final Set<String> roles;
// Standard constructor
public UserSession(String userId, Set<String> roles) {
this.userId = userId;
this.roles = new HashSet<>(roles);
}
// Copy Constructor (Prototype Implementation)
public UserSession(UserSession source) {
this.userId = source.userId;
this.roles = new HashSet<>(source.roles); // Deep copy of mutable collection
}
// Copy Factory Method
public static UserSession newInstance(UserSession source) {
return new UserSession(source);
}
}
3. Deep Cloning via Serialization / JSON
When dealing with deep, complex object graphs with nested lists, maps, and domain entities, manual deep copying becomes tedious and error-prone. Java developers frequently implement prototyping via serialization libraries (like Jackson or Gson):
public class DeepCloner {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static <T> T deepCopy(T source, Class<T> clazz) {
try {
byte[] bytes = MAPPER.writeValueAsBytes(source);
return MAPPER.readValue(bytes, clazz);
} catch (Exception e) {
throw new RuntimeException("Failed to clone prototype", e);
}
}
}
When to Use Prototype in Java
- High-Cost Initialization: When an object requires an expensive database query, disk I/O, or cryptographic computation to construct, keep one initialized instance in memory and clone it on demand.
- Complex Template Variants: In gaming, simulation, or batch pipelines where hundreds of entities share 95% of the same state (e.g., an
EnemySpawnercloning a pre-configuredOrcWarriorprototype with slightly adjusted HP). - Snapshotting & Undo Buffers: Creating isolated snapshots of an active state before executing operations that might need to be rolled back.
6. Singleton Pattern
The Singleton pattern is a creational design pattern that ensures a class has only one instance throughout an application's lifecycle while providing a global point of access to that instance.
Along with Factory, Abstract Factory, Builder, and Prototype, it completes the five canonical Gang of Four (GoF) creational patterns. In Java, however, it is uniquely scrutinized because naive implementations break easily under concurrency, reflection, and serialization.
Technical Invariants of a Singleton
- Controlled, Private Instantiation: The class's constructors are private to prevent direct instantiation via the
newkeyword from any outside class. - Static Self-Reference: The class maintains a
private staticreference to its single instantiated self. - Global Access Point: Access is provided strictly via a
public staticmethod (typicallygetInstance()), which controls when the instance is created (eager vs. lazy) and returns the existing reference on subsequent calls. - Thread-Safe Lifecycle: In concurrent environments, multiple threads calling
getInstance()concurrently must never result in multiple instances being allocated.
Idiomatic Java Implementations
Over the years, Java developers moved away from basic synchronized methods due to locking overhead. The three production-grade approaches are:
1. The Bill Pugh Singleton (Initialization-on-Demand Holder)
This is the cleanest approach for lazy initialization in pure Java. It leverages the JVM's class-loader mechanics: inner static classes are not loaded into memory until they are explicitly referenced.
public class ConnectionPool {
// 1. Private constructor prevents direct instantiation
private ConnectionPool() {
// Guard against reflection attack
if (InstanceHolder.INSTANCE != null) {
throw new IllegalStateException("Already initialized");
}
}
// 2. Static nested class loaded ONLY when getInstance() is called
private static class InstanceHolder {
private static final ConnectionPool INSTANCE = new ConnectionPool();
}
// 3. Thread-safe without explicit 'synchronized' blocks
public static ConnectionPool getInstance() {
return InstanceHolder.INSTANCE;
}
}
- Why it works: The Java Virtual Machine specification guarantees that class initialization (the static block/fields of
InstanceHolder) is serialized and thread-safe. You get lazy loading without locking overhead.
2. Double-Checked Locking (DCL)
Used when parameters must be passed during initialization or when working with legacy code. It minimizes synchronization overhead by checking for null before and after acquiring a lock.
public class CacheRegistry {
// Crucial: volatile prevents instruction reordering by JVM/CPU
private static volatile CacheRegistry instance;
private CacheRegistry() {}
public static CacheRegistry getInstance() {
// First check (no locking overhead)
if (instance == null) {
synchronized (CacheRegistry.class) {
// Second check (inside critical section)
if (instance == null) {
instance = new CacheRegistry();
}
}
}
return instance;
}
}
Why
volatileis mandatory: Creating an object in Java is not atomic. It involves:- Allocating memory.
- Running the constructor to initialize fields.
- Pointing the
instancevariable to that memory address.
Without
volatile, compilers and CPUs can reorder steps 2 and 3. Another thread could seeinstance != nullbefore constructor execution finishes, returning a half-initialized, corrupted object.
3. Enum Singleton (The Effective Java Standard)
In Effective Java, Joshua Bloch states that a single-element enum type is often the best way to implement a Singleton.
public enum AppConfiguration {
INSTANCE;
private String environment = "PRODUCTION";
public String getEnvironment() {
return environment;
}
public void setEnvironment(String environment) {
this.environment = environment;
}
}
// Client usage:
AppConfiguration.INSTANCE.getEnvironment();
- Why it is indestructible: The JVM guarantees that enum values are instantiated exactly once per classloader, thread-safely, and are inherently immune to reflection attacks and serialization duplication.
How Singletons Get Broken in Java (And How to Defend)
Traditional class-based Singletons can be bypassed through three JVM features:
| Vulnerability | How It Breaks the Singleton | How to Prevent It |
|---|---|---|
| Reflection | Constructor.setAccessible(true) can invoke a private constructor to create a 2nd instance. | Throw an exception inside the constructor if an instance already exists. |
| Serialization | Deserializing a byte stream creates an entirely new object in memory without calling constructors. | Implement the protected Object readResolve() hook and return getInstance(). |
| Multiple ClassLoaders | If two classloaders load the same class (e.g., in OSGi or legacy EAR deployments), each gets its own static variable. | Bind the singleton lifecycle to the application's root classloader or use an IoC container. |
The Modern Reality: Pattern vs. Container Scope
In modern enterprise architectures (Spring Boot, Quarkus, Micronaut), writing manual GoF Singleton classes is widely considered an anti-pattern:
- Hidden Dependencies: Calling
MyService.getInstance().doWork()inside business logic creates tight coupling and hides dependencies from the class contract. - Testing Nightmare: Global static state cannot be easily mocked, reset, or run in parallel across isolated unit tests.
- Single Responsibility Violation: The class manages its business logic and its own lifecycle/thread safety.
Instead, modern systems decouple the lifecycle from the class using Singleton Scope in a Dependency Injection (DI) container:
// The class is written as a standard POJO with a public constructor
@Service
public class OrderService {
// Easily mockable and injectable in unit tests: new OrderService()
}
The DI container (like Spring's ApplicationContext) takes responsibility for creating only one instance and passing it wherever required. The class remains clean, testable, and completely unaware that it is being used as a singleton.
Series Navigation
- ⬅️ Previous: Design Patterns: An Architectural Overview
- ➡️ Next: Design Patterns: Structural
- ⏭️ Part 4: Design Patterns: Behavioral