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)
Avoid pattern overuse:
-
Singleton: Global state makes code hard to test and parallelize. In Spring, every
@Component,@Service, and@Repositorybean is a singleton by default — you almost never need to implement Singleton yourself. -
Builder: Use Lombok
@Builderin production to eliminate boilerplate. For immutable objects, declare all fieldsfinaland omit setters. -
Factory: When creation logic is simple, a
switchexpression orMap<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();