SDE Path

Patterns I: Factory, Builder, Singleton

12 min read

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

  1. 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."

  2. 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."

  3. 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."

  4. 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.

Check yourself

5 questions — graded when you submit.

Question 1

When should you use the Factory pattern?

Question 2

You're designing a User object: name, email, age (all required). Should you use the Builder pattern?

Question 3

In the Logger singleton, why is the double-checked locking pattern written with a volatile keyword?

Question 4

A senior engineer suggests: 'Just use Singleton instead of passing Logger around as a dependency.' Why might this complicate testing?

Question 5

You're building a Config object with required fields (app name, env) and optional fields (log level, db pool size, cache ttl). Which pattern is most appropriate?

Finished reading?

Sign in to save your progress across lessons.