Skip to main content

Ch 17.2 Creational Patterns (Singleton, Factory, Builder)

1. Singleton — Ensuring a Single Instance

"Guarantees that only one instance of a class exists." Used when a single shared instance is needed globally — for database connections, loggers, and configuration managers.

Implementation (Lazy Initialization + Thread-safe)

public class DatabaseManager {
// volatile: ensures visibility across threads
private static volatile DatabaseManager instance;
private String connectionUrl;

private DatabaseManager() { // private constructor: prevents external creation
this.connectionUrl = "jdbc:mysql://localhost:3306/mydb";
System.out.println("DB Manager initialized");
}

// Double-Checked Locking: thread-safe + performance-efficient
public static DatabaseManager getInstance() {
if (instance == null) {
synchronized (DatabaseManager.class) {
if (instance == null) {
instance = new DatabaseManager();
}
}
}
return instance;
}

public void query(String sql) {
System.out.println("[" + connectionUrl + "] Query: " + sql);
}
}

// Usage
DatabaseManager db1 = DatabaseManager.getInstance();
DatabaseManager db2 = DatabaseManager.getInstance();
System.out.println(db1 == db2); // true (same instance)
db1.query("SELECT * FROM users");

Modern Java: Enum Singleton (Safest Approach)

// Immune to serialization attacks and reflection attacks
public enum AppConfig {
INSTANCE;

private final String appName = "MyApp";
private final int maxConnections = 10;

public String getAppName() { return appName; }
public int getMaxConnections() { return maxConnections; }
public void log(String message) {
System.out.println("[" + appName + "] " + message);
}
}

// Usage
AppConfig.INSTANCE.log("Application started");
System.out.println(AppConfig.INSTANCE.getMaxConnections()); // 10

2. Factory Method — Delegating Object Creation

"Delegates the decision of which class to instantiate to a factory." The client works with an interface and never needs to reference concrete implementation classes.

// Notification dispatch example
interface Notification {
void send(String message);
}

class EmailNotification implements Notification {
private final String email;
EmailNotification(String email) { this.email = email; }

@Override
public void send(String message) {
System.out.println("Email [" + email + "]: " + message);
}
}

class SmsNotification implements Notification {
private final String phone;
SmsNotification(String phone) { this.phone = phone; }

@Override
public void send(String message) {
System.out.println("SMS [" + phone + "]: " + message);
}
}

class PushNotification implements Notification {
private final String deviceId;
PushNotification(String deviceId) { this.deviceId = deviceId; }

@Override
public void send(String message) {
System.out.println("Push [" + deviceId + "]: " + message);
}
}

// Factory class
class NotificationFactory {
public static Notification create(String type, String target) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification(target);
case "SMS" -> new SmsNotification(target);
case "PUSH" -> new PushNotification(target);
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
}
}

// Usage — client never uses concrete classes directly
Notification n1 = NotificationFactory.create("EMAIL", "user@example.com");
Notification n2 = NotificationFactory.create("SMS", "+1-555-0100");
Notification n3 = NotificationFactory.create("PUSH", "device_abc123");

n1.send("Your order has been placed.");
n2.send("Your shipment is on the way.");
n3.send("You have a new message.");

3. Builder — Step-by-Step Complex Object Construction

"Separates the construction of a complex object from its representation." Especially useful when an object has many optional fields or a large number of constructor parameters.

The Problem: Telescoping Constructor Anti-Pattern

// Each new option requires yet another constructor — unmanageable
class Pizza {
Pizza(String size) { ... }
Pizza(String size, String crust) { ... }
Pizza(String size, String crust, boolean cheese) { ... }
Pizza(String size, String crust, boolean cheese, boolean tomato) { ... }
// Easy to confuse parameter order!
}

Builder Pattern Implementation

public class Pizza {
// Required fields
private final String size;
private final String crust;
// Optional fields
private final boolean cheese;
private final boolean pepperoni;
private final boolean mushroom;
private final boolean tomato;
private final String note;

private Pizza(Builder builder) {
this.size = builder.size;
this.crust = builder.crust;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.mushroom = builder.mushroom;
this.tomato = builder.tomato;
this.note = builder.note;
}

@Override
public String toString() {
return String.format(
"Pizza[%s, %s crust, cheese=%b, pepperoni=%b, mushroom=%b, tomato=%b, note='%s']",
size, crust, cheese, pepperoni, mushroom, tomato, note);
}

// Static inner Builder class
public static class Builder {
private final String size;
private final String crust;
private boolean cheese = false;
private boolean pepperoni = false;
private boolean mushroom = false;
private boolean tomato = false;
private String note = "";

public Builder(String size, String crust) {
this.size = size;
this.crust = crust;
}

public Builder cheese() { this.cheese = true; return this; }
public Builder pepperoni() { this.pepperoni = true; return this; }
public Builder mushroom() { this.mushroom = true; return this; }
public Builder tomato() { this.tomato = true; return this; }
public Builder note(String note) { this.note = note; return this; }

public Pizza build() { return new Pizza(this); }
}
}

// Usage: readable and mistake-resistant object construction
Pizza pizza1 = new Pizza.Builder("Large", "Thin")
.cheese()
.pepperoni()
.mushroom()
.note("Extra crispy please")
.build();

Pizza pizza2 = new Pizza.Builder("Medium", "Thick")
.cheese()
.tomato()
.build();

System.out.println(pizza1);
System.out.println(pizza2);

Lombok @Builder (Production Standard)

In real projects, use Lombok's @Builder to auto-generate all the boilerplate:

import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class User {
private String name;
private int age;
private String email;
@Builder.Default
private boolean active = true;
}

// Lombok generates the Builder class automatically
User user = User.builder()
.name("Alice")
.age(30)
.email("alice@example.com")
.build();

System.out.println(user); // User(name=Alice, age=30, email=alice@example.com, active=true)

Pro Tips

Avoid pattern overuse:

  • Singleton: Global state makes code hard to test and parallelize. In Spring, every @Component, @Service, and @Repository bean is a singleton by default — you almost never need to implement Singleton yourself.

  • Builder: Use Lombok @Builder in production to eliminate boilerplate. For immutable objects, declare all fields final and omit setters.

  • Factory: When creation logic is simple, a switch expression or Map<String, Supplier<T>> is often cleaner than a dedicated Factory class.

// Simple factory using a Map of suppliers
Map<String, Supplier<Notification>> registry = Map.of(
"email", () -> new EmailNotification("default@example.com"),
"sms", () -> new SmsNotification("+1-555-0000")
);
Notification n = registry.getOrDefault("email",
() -> { throw new IllegalArgumentException("Unknown type"); }).get();