Skip to content
GitHubLinkedin

Design Patterns: Creational

Engineering PracticesDesign Patterns • Part 2
#design-patterns#architecture#software-design#creational

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:

  1. Factory Method
  2. Abstract Factory
  3. Builder
  4. Prototype
  5. 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:

  1. 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.
  2. 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).
  3. 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).
  4. 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 switch statement 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.Connection creating Statement, PreparedStatement, and Blob)

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 RoleSLF4J AbstractionSLF4J Simple ImplementationLogback Implementation (logback-classic)
Product Interfaceorg.slf4j.Loggerorg.slf4j.simple.SimpleLoggerch.qos.logback.classic.Logger
Factory Interface (Creator)org.slf4j.ILoggerFactoryorg.slf4j.simple.SimpleLoggerFactorych.qos.logback.classic.LoggerContext
Factory MethodgetLogger(String name)Returns SimpleLogger instanceReturns 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:

  1. 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.
  2. 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).
  3. Composition-Based Client Decoupling: The client does not subclass the factory; it receives a reference to the AbstractFactory interface via dependency injection or composition. The client calls the various creation methods on that single factory instance to obtain its working ecosystem of products.
  4. 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 ConcreteFactory class without modifying existing client code or interfaces.
    • Closed (rigid) for new products: Introducing an entirely new product type requires modifying the AbstractFactory interface, which cascades breaking changes across all existing concrete factory implementations.

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 RoleJDBC AbstractionPostgreSQL Driver (pgjdbc)MySQL Driver (mysql-connector-j)
Abstract Factoryjava.sql.Connectionorg.postgresql.jdbc.PgConnectioncom.mysql.cj.jdbc.ConnectionImpl
Product Ajava.sql.Statementorg.postgresql.jdbc.PgStatementcom.mysql.cj.jdbc.StatementImpl
Product Bjava.sql.PreparedStatementorg.postgresql.jdbc.PgPreparedStatementcom.mysql.cj.jdbc.ClientPreparedStatement
Product Cjava.sql.CallableStatementorg.postgresql.jdbc.PgCallableStatementcom.mysql.cj.jdbc.CallableStatement
Product Djava.sql.DatabaseMetaDataorg.postgresql.jdbc.PgDatabaseMetaDatacom.mysql.cj.jdbc.DatabaseMetaData
Product Ejava.sql.Savepointorg.postgresql.jdbc.PSQLSavepointcom.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: Connection manufactures 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:

  1. Multiple distinct products: You aren't just making a Button; you are making a Button, a Checkbox, and a Scrollbar.
  2. Distinct suites or flavors: You have AWS vs. Azure (or PostgreSQL vs. MySQL, or DarkTheme vs. LightTheme).
  3. 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:

  1. 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(); }
    }
    
  2. 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

FeatureFactory MethodAbstract Factory
Primary IntentDecouples 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.
OutputProduces one product type (Logger, Transport, Document).Produces multiple related product types (Button + Checkbox + Scrollbar).
Core MechanismInheritance: 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 SignatureContains a single creation method: createProduct().Contains multiple creation methods: createProductA(), createProductB().
Adding a New Product FamilyNot 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 TypeEasy: 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 ExamplesILoggerFactory.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 TermWhat You Call It in Production CodeReal Example
AbstractFactoryFactory, Builder, Provider, Context, ConfigConnection, LoggerFactory, SecurityContextProvider
ConcreteFactoryDriver, Vendor Factory, Implementation ClassPgConnection, SimpleLoggerFactory
AbstractProductDomain Interface: Client, Service, Handler, RendererStatement, Logger, HttpClient, PdfRenderer
ConcreteProductThe specific underlying classPgStatement, SimpleLogger, WindowsButton
Product FamilyEcosystem, Suite, Driver pack, ThemePostgres JDBC Suite, AWS Provider Suite, Material Theme

Why "Abstract Product" Instead of "Product Interface"?

  1. 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.
  2. 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 no interface keyword.
  3. 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, Rust trait, Swift protocol, or Python ABC.

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

  1. 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.
  2. Elimination of the Telescoping Constructor Anti-Pattern: Avoids constructors with excessive ambiguous parameters (new Request(url, null, null, 5000, true, null, false)).
  3. 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.
  4. 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 (PdfDocumentBuilder vs. HtmlDocumentBuilder).
  • Director: Accepts a Builder instance and dictates the exact algorithm or sequence of calls (director.constructInvoice()).
  • Product: The resulting complex object retrieved via builder.getResult().

Comparison: Factory vs. Abstract Factory vs. Builder

DimensionFactory / Factory MethodAbstract FactoryBuilder
Creation StyleSingle-shot method invocationSingle-shot method invocations per familyMulti-step incremental assembly
Primary FocusWhat concrete subtype to instantiateWhich ecosystem/suite of products to instantiateHow to assemble a complex object with many options
Product StructureOften simple or uniformA family of independent componentsTypically a single, deeply configured or composite object
Immutability HandlingRequires all parameters at once in the constructorRequires all parameters per product constructorCollects 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

  1. Delegated Self-Duplication: The object itself handles its own replication via a method like clone() or copy(). This allows a client to duplicate an object without knowing its concrete class or private internal fields.
  2. 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.
  3. 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:

  1. Interface Contract Violation: java.lang.Cloneable is a marker interface containing zero methods. The actual clone() method is defined as protected on java.lang.Object. Implementing Cloneable simply flips an internal JVM flag to allow super.clone() to execute without throwing a CloneNotSupportedException.
  2. 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.
  3. 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 same permissions list reference. Mutating the list in the clone silently corrupts the original object.
  4. Final Fields Conflict: If you make fields final to 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 EnemySpawner cloning a pre-configured OrcWarrior prototype 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

  1. Controlled, Private Instantiation: The class's constructors are private to prevent direct instantiation via the new keyword from any outside class.
  2. Static Self-Reference: The class maintains a private static reference to its single instantiated self.
  3. Global Access Point: Access is provided strictly via a public static method (typically getInstance()), which controls when the instance is created (eager vs. lazy) and returns the existing reference on subsequent calls.
  4. 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 volatile is mandatory: Creating an object in Java is not atomic. It involves:

    1. Allocating memory.
    2. Running the constructor to initialize fields.
    3. Pointing the instance variable to that memory address.

    Without volatile, compilers and CPUs can reorder steps 2 and 3. Another thread could see instance != null before 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:

VulnerabilityHow It Breaks the SingletonHow to Prevent It
ReflectionConstructor.setAccessible(true) can invoke a private constructor to create a 2nd instance.Throw an exception inside the constructor if an instance already exists.
SerializationDeserializing a byte stream creates an entirely new object in memory without calling constructors.Implement the protected Object readResolve() hook and return getInstance().
Multiple ClassLoadersIf 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