Skip to main content
Advertisement

17.2 Creational Patterns (Singleton, Factory, Builder)

1. Singleton

"Ensure only one instance of a class exists." Used for database connections, loggers, and configuration managers.

// Thread-safe with Double-Checked Locking
public class DatabaseManager {
private static volatile DatabaseManager instance;

private DatabaseManager() { System.out.println("DB Manager initialized"); }

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("Query: " + sql); }
}

DatabaseManager db1 = DatabaseManager.getInstance();
DatabaseManager db2 = DatabaseManager.getInstance();
System.out.println(db1 == db2); // true

Modern Java: Enum Singleton (safest approach)

public enum AppConfig {
INSTANCE;
private final String appName = "MyApp";
public String getAppName() { return appName; }
public void log(String message) { System.out.println("[" + appName + "] " + message); }
}

AppConfig.INSTANCE.log("Application started");

2. Factory Method

"Delegate object creation to subclasses." The client code doesn't specify the exact class to create.

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 msg) { System.out.println("📧 Email to " + email + ": " + msg); }
}

class SmsNotification implements Notification {
private final String phone;
SmsNotification(String phone) { this.phone = phone; }
@Override public void send(String msg) { System.out.println("📱 SMS to " + phone + ": " + msg); }
}

class PushNotification implements Notification {
private final String deviceId;
PushNotification(String deviceId) { this.deviceId = deviceId; }
@Override public void send(String msg) { System.out.println("🔔 Push to " + deviceId + ": " + msg); }
}

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 notification type: " + type);
};
}
}

// Client code doesn't need to know concrete classes
Notification n1 = NotificationFactory.create("EMAIL", "user@example.com");
Notification n2 = NotificationFactory.create("SMS", "+1-555-1234");
n1.send("Order complete!");
n2.send("Shipment started!");

3. Builder

"Separate the construction of a complex object into steps." Especially useful when constructors have many parameters or many optional fields.

public class Pizza {
private final String size;
private final String crust;
private final boolean cheese;
private final boolean pepperoni;
private final boolean mushroom;
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.note = builder.note;
}

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

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 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 note(String note) { this.note = note; return this; }

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

// Readable, error-free object creation
Pizza pizza = new Pizza.Builder("Large", "Thin")
.cheese()
.pepperoni()
.note("Less spicy please")
.build();
System.out.println(pizza);

Pro Tip
  • Singleton: In Spring, beans are singletons by default via @Component, @Service — rarely need manual implementation.
  • Builder: In practice, use Lombok's @Builder annotation to eliminate boilerplate.
  • Factory: A switch expression or Map<String, Supplier<T>> pattern suffices for simple creation logic.
Advertisement