Skip to content

03. Creational patterns — build rooms without hardcoding every brick

~15 min read. Object creation looks harmless, until constructors start carrying the whole building.

Built on the ELI5 in 00-eli5.md. The room — a class or module with single responsibility — should be created cleanly, not assembled in random corners.


1) Why creation deserves design attention

See. Many LLD mistakes start before behavior even begins. We create the wrong object in the wrong place. Then every caller learns too many details.

A weak design often sounds like this. "Just new it here." Then another caller also news it. Then another caller sets half the fields manually. Soon object creation rules are scattered everywhere.

That hurts for four reasons.

  1. Validation logic gets duplicated.
  2. Caller knows too many concrete classes.
  3. Optional fields make constructors unreadable.
  4. Tests become noisy and fragile.

So what to do? Use patterns when creation has shape. Do not use them by habit. Use them when plain constructors start leaking policy.

A quick mental map helps.

Need one family of variants?      → Abstract Factory
Need one method choosing subtype? → Factory Method
Need many optional steps?         → Builder
Need one controlled instance?     → Singleton

That is your floor plan for object creation. Not every object needs a pattern. Only the ones carrying repeated creation pain.

2) Factory Method and Abstract Factory

Factory Method solves one focused question. Which subtype should I create here? Caller should not decide every time.

Example: notification channel.

interface NotificationSender {
    void send(String userId, String text);
}

class EmailSender implements NotificationSender { ... }
class SmsSender implements NotificationSender { ... }

class NotificationFactory {
    NotificationSender create(String channel) {
        return switch (channel) {
            case "email" -> new EmailSender();
            case "sms" -> new SmsSender();
            default -> throw new IllegalArgumentException();
        };
    }
}

Now caller only asks the factory. One room creates channel-specific senders. Another room uses them. Responsibilities stay separate.

When do we upgrade to Abstract Factory? When objects come in families that must match. For example, UI theme components. Light theme button and light theme dialog should align. Do not mix light button with dark dialog accidentally.

interface UiFactory {
    Button createButton();
    Dialog createDialog();
}

class LightUiFactory implements UiFactory { ... }
class DarkUiFactory implements UiFactory { ... }

See the difference. Factory Method picks one product. Abstract Factory gives a related family of products. Simple, no?

Worked example with numbers. Suppose app supports 3 themes. Each theme needs 4 widgets. Without Abstract Factory, you have 12 direct constructor choices spread across screens. With one factory per theme, caller picks theme once, then gets all 4 matching widgets safely.

3) Builder and Singleton

Builder shines when object assembly has many optional fields. Imagine creating a hotel booking search request. Mandatory: city, checkIn, checkOut. Optional: guests, breakfast, lateCheckout, coupon, loyaltyTier.

Bad constructor:

new SearchRequest("Goa", in, out, 2, true, false, "SAVE20", "GOLD");

What is true here? Breakfast? Insurance? Nobody knows after two weeks.

Builder makes intent visible.

SearchRequest request = SearchRequest.builder()
    .city("Goa")
    .checkIn(in)
    .checkOut(out)
    .guests(2)
    .breakfastIncluded(true)
    .coupon("SAVE20")
    .build();

Now creation reads like a form. Validation can run in build(). That centralizes rules nicely. The plumbing of values becomes clear.

Next, Singleton. This one is famous and overused. Use it only when you truly need one shared coordinator. Examples: configuration cache, process-wide metrics registry, feature flag snapshot loader.

class ConfigRegistry {
    private static final ConfigRegistry INSTANCE = new ConfigRegistry();

    private ConfigRegistry() { }

    static ConfigRegistry getInstance() {
        return INSTANCE;
    }
}

Why not everywhere? Because singleton hides global state. Global state makes tests awkward. It also smuggles dependencies across the hallway.

So what to do? Prefer dependency injection first. Choose singleton only when one shared instance is truly part of the domain or process model.

4) Choosing the right creation pattern

Use this quick table in interviews.

Problem                                    → Pattern
Choose one subtype by input                → Factory Method
Choose one matching family of products     → Abstract Factory
Assemble many optional fields step by step → Builder
Enforce one controlled shared instance     → Singleton

Now let us compare with a food delivery example.

  • Restaurant card theme selection → Abstract Factory.
  • Payment handler by method name → Factory Method.
  • Checkout request with many fields → Builder.
  • In-memory app config loader → Singleton, maybe.

A worked number example helps more. Suppose checkout request has 3 mandatory fields and 7 optional. Constructors create 2^7 = 128 possible combinations. You will not write 128 overloads. Builder is the sane answer.

For channel creation, suppose you support 5 channels. Each caller picks channel 20 times across the codebase. That is 100 spots knowing concrete classes. Move the choice into one factory. Now only one place knows the mapping.

One caution. Do not use Abstract Factory when a simple map works. Do not use Singleton when a normal application-scoped object works. Pattern fit matters more than pattern vocabulary.


Where this lives in the wild

  • At Shopify, a backend engineer uses builders for discount rule requests with many optional constraints.
  • At Uber, a mobile platform engineer uses abstract factories for rider and driver app theme component families.
  • At Zerodha, a backend engineer uses factory methods to choose broker adapters by exchange type.
  • At Adobe, a document services engineer uses builders for export jobs with watermark, page range, and format options.
  • At Atlassian, a platform engineer keeps one process-wide feature flag cache with controlled initialization semantics.

Pause and recall

  1. When does Factory Method become more useful than direct constructors?
  2. Why is Builder safer than many overloaded constructors?
  3. What is the exact difference between Factory Method and Abstract Factory?
  4. Why can Singleton silently increase testing pain?

Interview Q&A

Why Factory Method not direct new in every caller?

Because subtype choice is a policy, not scattered caller knowledge. Centralizing that choice reduces duplication and regressions. Common wrong answer to avoid: "Factory is mainly to make code look advanced."

Why Abstract Factory not several unrelated factories?

Because related products must stay compatible as one family. One family selector preserves that consistency. Common wrong answer to avoid: "Abstract Factory is just a bigger factory method."

Why Builder not telescoping constructors?

Because optional combinations explode and parameter order becomes unreadable. Builder makes intent and validation explicit. Common wrong answer to avoid: "Constructors are always simpler because they are native."

Why Singleton not global static helpers everywhere?

Because uncontrolled global state hides dependencies and breaks tests. Singleton should represent a deliberate one-instance policy. Common wrong answer to avoid: "If many callers need it, make it singleton."


Apply now (5 min)

Exercise: Design creation for a ReportExportJob with format, filters, optional watermark, optional compression, and delivery channel. Choose one creational pattern and justify it in three lines. Then choose another pattern for delivery channel selection.

Sketch from memory: Draw the four-pattern selection table. For each row, write one product example from daily software. Keep the examples concrete.


Bridge. Now objects exist; next question is how to connect them cleanly without tearing walls. → 04-structural-patterns.md