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
@Builderannotation to eliminate boilerplate. - Factory: A
switchexpression orMap<String, Supplier<T>>pattern suffices for simple creation logic.