Creational patterns solve one problem: how to construct objects cleanly. Interviewers test whether you know when each pattern earns its weight and when it's over-engineering.
Factory Pattern
When it applies: You have multiple types of a thing, and the caller shouldn't know which concrete class to instantiate.
Classic trap: Using Factory when there's only one implementation. Defer the abstraction until you need it.
Motivation: Multiple Implementations
// Without Factory: the caller knows too much
if (dbType.equals("postgres")) {
Database db = new PostgresDatabase();
} else if (dbType.equals("mysql")) {
Database db = new MySQLDatabase();
} else {
db = new SQLiteDatabase();
}
This if-else spreads across callers. Adding MongoDB means finding and editing every callsite. Violates OCP.
The Fix: Factory
interface Database {
void connect();
void query(String sql);
}
class PostgresDatabase implements Database {
@Override
public void connect() { /* postgres setup */ }
@Override
public void query(String sql) { /* postgres query */ }
}
class MySQLDatabase implements Database {
@Override
public void connect() { /* mysql setup */ }
@Override
public void query(String sql) { /* mysql query */ }
}
class DatabaseFactory {
public static Database create(String type) {
switch (type) {
case "postgres": return new PostgresDatabase();
case "mysql": return new MySQLDatabase();
case "sqlite": return new SQLiteDatabase();
default: throw new IllegalArgumentException("Unknown DB: " + type);
}
}
}
// Caller: simple, ignorant of concrete types
Database db = DatabaseFactory.create(config.getDbType());
db.connect();
Cost: One extra class. Benefit: Centralized instantiation logic, easy to add types, callers are clean.
Builder Pattern
When it applies: Object construction is complex: many optional fields, validation order matters, or you want to build incrementally.
Classic trap: Using Builder for simple objects (String name, int age). That's over-engineering.
Without Builder: Constructor Hell
// Constructor overloading spiral:
class Report {
Report(String title) { }
Report(String title, String format) { }
Report(String title, String format, boolean hasCharts) { }
Report(String title, String format, boolean hasCharts, String footer) { }
// ... and so on
}
// Or, the telescoping mess:
Report r = new Report("Q4 Sales", "PDF", true, "Footer Text", true, false, "Arial");
// What does the 6th boolean mean? Terrible.
The Fix: Builder
class Report {
private final String title;
private final String format;
private final boolean hasCharts;
private final String footer;
private final String fontFamily;
private final int fontSize;
private Report(Builder builder) {
this.title = builder.title;
this.format = builder.format;
this.hasCharts = builder.hasCharts;
this.footer = builder.footer;
this.fontFamily = builder.fontFamily;
this.fontSize = builder.fontSize;
}
public static class Builder {
private final String title; // required
private String format = "PDF"; // defaults
private boolean hasCharts = false;
private String footer = "";
private String fontFamily = "Arial";
private int fontSize = 12;
public Builder(String title) {
this.title = title;
}
public Builder format(String format) {
this.format = format;
return this;
}
public Builder hasCharts(boolean hasCharts) {
this.hasCharts = hasCharts;
return this;
}
public Builder footer(String footer) {
this.footer = footer;
return this;
}
public Builder fontFamily(String fontFamily) {
this.fontFamily = fontFamily;
return this;
}
public Builder fontSize(int fontSize) {
if (fontSize < 8 || fontSize > 72) throw new IllegalArgumentException();
this.fontSize = fontSize;
return this;
}
public Report build() {
return new Report(this);
}
}
}
// Usage: clear, fluent, type-safe
Report r = new Report.Builder("Q4 Sales")
.format("Excel")
.hasCharts(true)
.fontFamily("Helvetica")
.fontSize(14)
.build();
Cost: More code (Builder class). Benefit: Readable construction, optional fields, validation in one place, no ambiguous positional args.
Singleton Pattern
When it applies: You need exactly one instance of something: a config manager, a logger, a connection pool. And passing it around as a parameter is too noisy.
Classic trap: Singletons are hard to test and hide global state. Modern advice: usually avoid, use dependency injection instead.
The Naive Singleton (Not Thread-Safe)
class Logger {
private static Logger instance;
private Logger() { } // private constructor
public static Logger getInstance() {
if (instance == null) {
instance = new Logger(); // NOT thread-safe
}
return instance;
}
}
Problem: In multi-threaded code, two threads can both pass the null check and create two Logger instances. Singleton broken.
Thread-Safe Singleton: Eager Initialization
class Logger {
private static final Logger instance = new Logger();
private Logger() { }
public static Logger getInstance() {
return instance;
}
}
Pro: Thread-safe, simple. Con: instance is created at class load time, even if never used.
Thread-Safe Singleton: Lazy Initialization (Double-Checked Locking)
class Logger {
private static volatile Logger instance;
private Logger() { }
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger();
}
}
}
return instance;
}
}
Pro: Lazy, thread-safe. Con: Double-checked locking is subtle; easy to get wrong. volatile is required.
The DI Alternative (Preferred in Modern Code)
class Application {
private final Logger logger;
// Inject Logger as a dependency
public Application(Logger logger) {
this.logger = logger;
}
public void run() {
logger.info("Starting application");
}
}
// At app startup, a DI container ensures one Logger instance is created and wired:
Logger logger = new Logger(); // created once
Application app = new Application(logger);
app.run();
Why DI is better:
- Easy to test: pass a mock Logger.
- Explicit: no global state magic.
- Flexible: if you later need two loggers (for dev and production), no refactoring needed.
When NOT to Use These Patterns
- Factory: Only one implementation? Don't use it. Wait until a second type appears or tests need a mock.
- Builder: Simple 2-3 field object? Use a constructor. Builder shines when fields are 5+.
- Singleton: If you can pass the object as a parameter, do that. Singletons hide dependencies and complicate testing.
In the interview
-
Name the pattern and defend it. "I'll use Factory here so we can add new database types without touching the caller. This respects OCP."
-
Be honest about trade-offs. "Builder makes construction clearer but adds a class. I'll use it because config has 8 fields and this is the clearest way."
-
Suggest DI over Singleton. When they ask about Singleton, say: "I'd use DI—inject the logger. Easier to test, no global state magic. Singleton if the framework requires it."
-
Watch for over-engineering. If you're using Factory for one concrete class, pause. If Builder is for 2 fields, constructor suffices. Only use patterns when they solve a real problem.