Creational Patterns
Singleton, Factory, Abstract Factory, Builder, and Prototype — how to control object creation cleanly.
Introduction
Creational design patterns answer a core LLD question: who is allowed to create objects, when are they created, and how do callers avoid coupling to construction details.
In interviews at Amazon, Microsoft, and Google, these patterns show whether you can separate object creation from business logic. Singleton controls one shared instance, Factory Method delegates one product creation step to subclasses, Abstract Factory creates related product families, Builder assembles complex objects safely, and Prototype copies configured objects without rebuilding them from scratch.
Learning Objectives
Explain the intent, use cases, and pitfalls of Singleton, Factory Method, Abstract Factory, Builder, and Prototype.
Implement thread-safe Singleton variants in Java: eager initialization, double-checked locking, enum singleton, and Bill Pugh holder.
Choose Factory Method or Abstract Factory based on whether one product or a related family of products must vary.
Use Builder to avoid telescoping constructors and preserve invariants for complex immutable objects.
Use Prototype when cloning a preconfigured object is cheaper or clearer than rebuilding it.
Call out interview tradeoffs, especially Singleton as a global-state anti-pattern when overused.
Core Theory
Creational patterns isolate object construction
A creational pattern is useful when the client should not know the exact class, constructor sequence, dependency wiring, or configuration needed to create an object.
The design goal is not to hide every constructor. The goal is to put creation logic at the boundary where variation belongs:
- A single controlled instance belongs behind Singleton only when shared identity is required.
- A single varying product belongs behind Factory Method.
- A family of compatible products belongs behind Abstract Factory.
- A multi-step object with optional fields belongs behind Builder.
- A copied configured baseline belongs behind Prototype.
In LLD rounds, state the creation problem first, then introduce the pattern. That makes the pattern feel like a design consequence rather than memorized vocabulary.
Singleton: one controlled instance
Intent: Ensure exactly one instance of a class is used where a shared identity is required, and provide a controlled access point.
Thread-safe variants: Eager initialization is simple and safe but creates the object even if unused. Double-checked locking is lazy and safe only with a volatile field. Enum singleton is concise, serialization-safe, and reflection-resistant for most practical cases. Bill Pugh holder uses class loading to get lazy initialization without explicit synchronization.
When to use: Configuration registries, process-wide metrics collectors, stateless service locators in legacy code, and infrastructure objects that truly have one process-level identity.
Pitfalls: Singleton often becomes a global-state anti-pattern. It hides dependencies, makes tests order-dependent, complicates parallel tests, and encourages unrelated classes to reach into shared mutable state. Prefer dependency injection unless one-instance identity is the actual requirement.
public final class EagerConfig {
private static final EagerConfig INSTANCE = new EagerConfig();
private EagerConfig() {
}
public static EagerConfig getInstance() {
return INSTANCE;
}
}
public final class DclConfig {
private static volatile DclConfig instance;
private DclConfig() {
}
public static DclConfig getInstance() {
DclConfig local = instance;
if (local == null) {
synchronized (DclConfig.class) {
local = instance;
if (local == null) {
local = new DclConfig();
instance = local;
}
}
}
return local;
}
}
public enum EnumConfig {
INSTANCE;
public String region() {
return "us-east-1";
}
}
public final class HolderConfig {
private HolderConfig() {
}
private static class Holder {
private static final HolderConfig INSTANCE = new HolderConfig();
}
public static HolderConfig getInstance() {
return Holder.INSTANCE;
}
}Factory Method: defer one product choice
Intent: Define an operation for creating one product, but let subclasses or specialized creators decide the concrete product class.
When to use: A workflow is stable but one created object varies, such as notification channels, document parsers, payment processors, exporters, or storage adapters. The base creator owns the common flow, and subclasses supply the product.
Pitfalls: Do not create a factory hierarchy when a simple constructor or static helper is enough. Factory Method can create many small classes, and it is easy to confuse it with Abstract Factory. If you are producing one product type, Factory Method is likely enough; if you are producing a family of compatible products, consider Abstract Factory.
interface Notification {
void send(String message);
}
final class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
final class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
abstract class NotificationCreator {
public final void notifyUser(String message) {
Notification notification = createNotification();
notification.send(message);
}
protected abstract Notification createNotification();
}
final class EmailCreator extends NotificationCreator {
@Override
protected Notification createNotification() {
return new EmailNotification();
}
}
final class SmsCreator extends NotificationCreator {
@Override
protected Notification createNotification() {
return new SmsNotification();
}
}Abstract Factory: create compatible families
Intent: Provide an interface for creating related products without binding clients to their concrete classes.
When to use: A client must work with a product family that must stay compatible, such as light theme widgets, dark theme widgets, cloud provider clients, database-specific repositories, or platform-specific UI controls. The client receives a factory and asks it for each related product.
Pitfalls: Abstract Factory can become heavy if there are too many products or frequent additions to the family. Adding a new product type requires changing every factory implementation. Use it when family compatibility is important enough to justify that structure.
interface Button {
void render();
}
interface Checkbox {
void render();
}
interface UiFactory {
Button createButton();
Checkbox createCheckbox();
}
final class LightButton implements Button {
@Override
public void render() {
System.out.println("Light button");
}
}
final class LightCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Light checkbox");
}
}
final class DarkButton implements Button {
@Override
public void render() {
System.out.println("Dark button");
}
}
final class DarkCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Dark checkbox");
}
}
final class LightThemeFactory implements UiFactory {
@Override
public Button createButton() {
return new LightButton();
}
@Override
public Checkbox createCheckbox() {
return new LightCheckbox();
}
}
final class DarkThemeFactory implements UiFactory {
@Override
public Button createButton() {
return new DarkButton();
}
@Override
public Checkbox createCheckbox() {
return new DarkCheckbox();
}
}
final class SettingsScreen {
private final UiFactory factory;
SettingsScreen(UiFactory factory) {
this.factory = factory;
}
public void render() {
factory.createButton().render();
factory.createCheckbox().render();
}
}Builder: construct complex objects safely
Intent: Separate construction of a complex object from its representation so callers can assemble it step by step while the object remains valid when built.
When to use: An object has many optional fields, validation spans multiple fields, construction should read fluently, or immutable objects would otherwise need many overloaded constructors. Builder is common for requests, configurations, test fixtures, and domain objects with optional attributes.
Pitfalls: Builder is overkill for small objects with two or three required fields. A weak builder can also delay validation too long or allow partially valid states to leak. Keep the built object immutable and validate in build.
import java.util.List;
public final class OrderRequest {
private final String customerId;
private final List<String> itemIds;
private final String couponCode;
private final boolean giftWrap;
private OrderRequest(Builder builder) {
this.customerId = builder.customerId;
this.itemIds = List.copyOf(builder.itemIds);
this.couponCode = builder.couponCode;
this.giftWrap = builder.giftWrap;
}
public static Builder builder(String customerId, List<String> itemIds) {
return new Builder(customerId, itemIds);
}
public static final class Builder {
private final String customerId;
private final List<String> itemIds;
private String couponCode;
private boolean giftWrap;
private Builder(String customerId, List<String> itemIds) {
this.customerId = customerId;
this.itemIds = itemIds;
}
public Builder couponCode(String couponCode) {
this.couponCode = couponCode;
return this;
}
public Builder giftWrap(boolean giftWrap) {
this.giftWrap = giftWrap;
return this;
}
public OrderRequest build() {
if (customerId == null || customerId.isBlank()) {
throw new IllegalArgumentException("customerId is required");
}
if (itemIds == null || itemIds.isEmpty()) {
throw new IllegalArgumentException("at least one item is required");
}
return new OrderRequest(this);
}
}
}Prototype: copy a configured baseline
Intent: Create new objects by copying an existing prototype instead of constructing from scratch.
When to use: Object creation is expensive, setup is repetitive, or the starting configuration is known at runtime. Examples include document templates, game object templates, workflow definitions, form schemas, and preconfigured rules.
Pitfalls: Cloning can be dangerous when objects hold mutable nested state. Decide between shallow copy and deep copy explicitly. Avoid Java Object clone unless you can explain its limitations; a copy method or copy constructor is often clearer and safer.
import java.util.List;
interface DocumentPrototype {
DocumentPrototype copy();
}
public final class ReportTemplate implements DocumentPrototype {
private final String title;
private final List<String> sections;
public ReportTemplate(String title, List<String> sections) {
this.title = title;
this.sections = List.copyOf(sections);
}
@Override
public ReportTemplate copy() {
return new ReportTemplate(title, sections);
}
public ReportTemplate withTitle(String newTitle) {
return new ReportTemplate(newTitle, sections);
}
}
final class ReportService {
private final ReportTemplate monthlyTemplate;
ReportService(ReportTemplate monthlyTemplate) {
this.monthlyTemplate = monthlyTemplate;
}
public ReportTemplate createMonthlyReport(String title) {
return monthlyTemplate.copy().withTitle(title);
}
}Choosing the right creational pattern
Start from the variation point. If there is no variation and construction is simple, use a constructor. If construction varies by one product, use Factory Method. If compatible families vary together, use Abstract Factory. If construction has many optional steps, use Builder. If a runtime template should be copied, use Prototype.
Treat Singleton as the exception. It answers an identity constraint, not a general dependency-management problem. In modern Java systems, dependency injection often gives one shared instance per container while keeping dependencies explicit and testable.
Diagrams
Builder separates step-by-step assembly from the final object
Comparisons
Factory Method vs Abstract Factory
| Dimension | Factory Method | Abstract Factory | Interview guidance |
|---|---|---|---|
| Main problem | One product choice varies inside a larger workflow | A family of related products must vary together | Ask whether callers need one product or multiple compatible products |
| Typical shape | An abstract creator defines a creation method | A factory interface exposes several creation methods | Factory Method is usually smaller and easier to introduce |
| Client dependency | Client may use a creator subclass for the product it needs | Client receives one family factory and creates related products from it | Abstract Factory keeps product compatibility centralized |
| Adding a new product family | Often add a new creator subclass | Add a new concrete factory implementing the same family methods | Both handle new variants well when interfaces are stable |
| Adding a new product type | Usually unrelated unless the single product abstraction changes | Requires updating the factory interface and all concrete factories | Abstract Factory is less convenient when the family keeps gaining new product types |
| Example | EmailCreator creates EmailNotification | DarkThemeFactory creates DarkButton and DarkCheckbox | Use examples that make the scope of creation obvious |
Builder vs telescoping constructors
| Dimension | Builder | Telescoping constructors | Interview guidance |
|---|---|---|---|
| Readability | Named fluent steps reveal the meaning of each optional value | Many overloaded constructors force callers to remember argument order | Use Builder when constructor calls become hard to read |
| Validation | Central validation can run once in build | Validation is duplicated or scattered across overloads | Builder works well when fields have cross-field constraints |
| Immutability | Final object can be immutable while the builder is mutable | Final object can also be immutable but construction is less expressive | Prefer immutable built objects for domain requests and configs |
| Cost | Adds an extra class or nested class | Has fewer types for small objects | Do not introduce Builder for tiny value objects |
| Evolution | New optional fields can be added with new builder methods | New optional fields often create more overloads | Builder is safer for APIs that evolve over time |
Best Practices
- ✓
Use the simplest construction mechanism that keeps callers decoupled and the object valid.
- ✓
Explain the creation variation before naming the pattern in an interview.
- ✓
Keep constructors visible when construction is simple; not every class needs a factory.
- ✓
Prefer dependency injection over Singleton when you only need shared service reuse.
- ✓
Make Singleton instances immutable or carefully synchronized if they hold state.
- ✓
For double-checked locking, use a volatile instance field or choose a simpler Singleton variant.
- ✓
Use Abstract Factory only when product compatibility across a family matters.
- ✓
Validate required fields in Builder build and return immutable objects.
- ✓
For Prototype, document whether copying is shallow or deep and protect mutable nested collections.
Common Mistakes
- ×
Using Singleton as a convenient global variable instead of passing dependencies explicitly.
- ×
Writing double-checked locking without volatile, which can expose unsafe publication.
- ×
Adding factories for every class and making the design harder to navigate than constructors.
- ×
Using Factory Method for a whole product family and scattering compatibility rules across creators.
- ×
Using Abstract Factory when only one product varies, creating unnecessary interfaces.
- ×
Creating a Builder that allows build to return an invalid object.
- ×
Forgetting defensive copies in Builder or Prototype when lists and maps are mutable.
- ×
Using Java Object clone without handling deep copy, final fields, and constructor invariants clearly.
Quiz
0/7 answered
1.Which Singleton variant is usually the simplest serialization-safe Java choice?
2.Why must the instance field be volatile in double-checked locking?
3.When should Factory Method be preferred over Abstract Factory?
4.What is the primary benefit of Abstract Factory?
5.Which object is the best fit for Builder?
6.What is the main risk in Prototype?
7.What is the strongest critique of overusing Singleton?
Flashcards
Cheat Sheet
Creational pattern goal: Keep construction logic near the variation point and away from business workflows.
Singleton: Use only for true one-instance identity. Prefer enum or Bill Pugh holder for simple thread safety. Use double-checked locking only with volatile. Avoid mutable global state.
Factory Method: One product varies. The base creator owns the workflow and delegates product creation to a method implemented by subclasses or specialized creators.
Abstract Factory: A family of products varies together. The client receives one factory and creates compatible products from that family.
Builder: Many required and optional construction inputs. Builder collects values, validates in build, and returns an immutable final object.
Prototype: Copy a configured object when rebuilding is expensive or when templates are selected at runtime. Be explicit about shallow versus deep copying.
Interview shortcut: Constructor for simple creation, Factory Method for one varying product, Abstract Factory for related families, Builder for complex construction, Prototype for copying, Singleton only for genuine shared identity.
References
- BookDesign Patterns: Elements of Reusable Object-Oriented Software — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides
- BookEffective Java — Joshua Bloch
- BlogRefactoring Guru: Creational Design Patterns
- DocsOracle Java Tutorials: Enum Types