Study Notes
Design & Patternsapplied

Factory Method Pattern

The Factory Method creational pattern — intent, structure, when to use it, implementation with abstract creators, and real-world Java examples.

30 min read·verified Java 21·reviewed 2026-08-30

After this you should be able to

  • Recognize the factory method seam
  • Extend creation without changing callers
Design PatternsCreationalFactory

My full reference on design patterns and the Factory Method, built so I never have to go back to the original material. Hand-copy notes up top, deep explanations and runnable Java in the middle, a practice lab at the bottom.

creational pattern OOP Java loose coupling open/closed
I.

Notebook Summary  (copy by hand) #

Design patterns, the big idea#

DESIGN PATTERNS = reusable templates for common design problems
- tried &true, proven solutions (not invented per-project)
- "cookbook for developers" → recipe = template, not finished dish
- built on OOP + design principles (SOLID etc.)
- NOT copy-paste code → a *shape* you adapt to your problem

3 families (GoF, 23 patterns total):
  Creational  → how objects are MADE   (Factory, Singleton, Builder...)
  Structural  → how objects are COMPOSED (Adapter, Decorator...)
  Behavioral  → how objects TALK       (Observer, Strategy...)
doodle: 3 labeled boxes [Create][Structure][Behave] = 3 GoF families
                    

Why we need them#

                        cook w/o recipe = waste time, fail, retry
recipe (others' experience) →
                        fast, tested, repeatable
SAME for code: don't re-solve solved problems
benefits: shared vocab • maintainable • flexible to change • fewer bugs
doodle: stick figure staring at empty pan vs same figure holding a book = no-recipe vs cookbook
                    

Factory Method, definition + intent#

FACTORY METHOD = delegate object creation to a separate component
key rules:
  - NO direct `new` in client → ask the factory instead
  - choose concrete type DYNAMICALLY at runtime (by input)
  - client codes to an INTERFACE, never to concrete classes
intent: decouple "what to use" from "how to build it"
doodle: client → [FACTORY box] → pops out 1 of 3 shapes (□ ○ △)
                    

The 4 roles (memorise these)#

                        P-C-F-C
                        ("Please Create Factory, Client")
  Product          = interface / abstract class (the contract)
  ConcreteProduct  = real classes implementing Product
  ProductFactory   = has the factory method that returns a Product
  Client           = wants a Product, asks Factory, never uses `new`
doodle: Client →(asks)→ Factory →(returns)→ Product ←(implements)— [PA][PB][PC]
                    

Compiler example, solution ladder#

StageWhat it doesProblem
Naive one class, if/else on language, private compileX() methods add C# = edit core class; not extensible
OKseparate JavaCompiler/CppCompiler... held as fields, still if/elselogic split but client still glued to concretes
Factory Compilation interface + CompilationFactory.getInstance(lang) add C#: new class + 1 case, client untouched

Naive vs Factory, contrast#

Naive if/elseFactory
Couplingtight (client knows all)loose (knows interface)
Add new typeedit existing classadd class + 1 case
OCPviolatedmostly satisfied
Testinghard to mockeasy (swap factory)

Core code skeleton#

interface Compilation { void compile(String code); }

class JavaCompiler   implements Compilation { compile(){...} }
class CppCompiler    implements Compilation { compile(){...} }
class PythonCompiler implements Compilation { compile(){...} }

class CompilationFactory {
  static Compilation getInstance(String lang){
    switch(lang){ case "JAVA": return new JavaCompiler(); ... }
  }
}
// client:
Compilation c = CompilationFactory.getInstance(lang);
c.compile(code);   // no `new`, no if/else

WhatsApp Status, map the roles#

RoleMaps to
Product Status interface (e.g. display() )
ConcreteProduct TextStatus, ImageStatus, VideoStatus
ProductFactory StatusFactory.getStatus(type)
ClientWhatsApp status feature (posts, visible 24h)

Gotchas / recall hooks#

- "Factory Method" (GoF) uses INHERITANCE/subclassed creators;
  the switch-style one above is the common "Simple Factory" idiom. [+]
  - Factory still uses `new` INSIDE it → just hidden from client
- adding a case = touches factory (small OCP leak) → registry map fixes it [+]
- return TYPE = interface; return VALUE = concrete
doodle: light switch labeled `new` hidden inside the FACTORY box, client can't see it
                    
II.

Detailed Study Notes#

1. What a design pattern actually is #

A design pattern is a reusable, proven template for solving a problem that keeps recurring in software design, not a finished piece of code I can drop in.

Imagine I'm hungry, I can't cook, and ordering food isn't an option. I could flail around the kitchen, burn a few attempts, and slowly figure it out. Or I could open a cookbook and follow a recipe somebody already perfected. The recipe isn't the meal; it's a tested set of steps I adapt to what's in my fridge. Design patterns are exactly that cookbook for software: experienced developers hit the same structural problems again and again, found solutions that hold up, and wrote down the shape of those solutions so the rest of us don't re-discover them the hard way.

The critical nuance is that a pattern is a template, not copy-paste code. It describes the roles, relationships, and responsibilities, and I fill in the concrete classes for my specific situation. Patterns lean heavily on object-oriented principles (encapsulation, polymorphism, abstraction) and on design principles like SOLID. They give teams a shared vocabulary too, saying "let's use a Factory here" instantly communicates an entire structure.

+ The classic catalog is the "Gang of Four" (GoF) book, Design Patterns: Elements of Reusable Object-Oriented Software (1994), which defined 23 patterns in three families. Factory Method is one of them, in the Creational family.
FamilyConcerned withExamples
Creational + how objects get createdFactory Method, Abstract Factory, Singleton, Builder, Prototype
Structural + how objects are composed into larger structuresAdapter, Decorator, Facade, Proxy, Composite
Behavioral + how objects communicate & share responsibilityObserver, Strategy, Command, State, Iterator
Why it matters Patterns turn "I have a weird design problem" into "this is a known problem with a known answer." That speeds up design, reduces bugs, and makes code readable to anyone who knows the vocabulary.
Common misconception A pattern is not a library or a snippet. Two Factory implementations can look quite different in code yet be the same pattern, because the pattern is the relationship between roles, not the literal lines.

2. Why we even need patterns #

Patterns exist so we stop re-solving problems that have already been solved well.

Going back to the kitchen: cooking blind wastes time and effort and usually fails the first few times. Reusing someone's recipe gets a good result fast and repeatably. Software has the same dynamic. The problems of "how do I create objects flexibly," "how do I let one object watch another," "how do I add behavior without editing a class" have all been chewed on for decades. Reinventing those answers per project is wasted effort and a source of subtle bugs.

Concretely, patterns buy us: maintainability (a new dev recognizes the structure), flexibility (the design absorbs change without rewrites), a shared language, and fewer defects because the structure has been battle-tested.

Interview angle Don't just name a pattern, justify it. "I used Factory because the set of compiler types will grow and I wanted the client decoupled from concrete classes" beats "I used Factory because it's a creational pattern." Also be ready for the flip side: over-using patterns adds indirection and ceremony where a plain class would do.

3. The motivating problem, a multi-language compiler #

The goal: build a compiler service that takes code plus a language name and compiles it, where the language can be Java, C++, or Python (and might grow).

The intended API is dead simple:

target API
class Compiler {
  public void compile(String code, String language) { }
}
// language ∈ { "JAVA", "C++", "PYTHON" } — how do we implement this?

The whole session is about answering "how do you implement compile well?" and walking up a ladder of increasingly better designs. Jump to the code ladder ↓

4. Solution 1, the naive if/else approach #

Put everything in one class and branch on the language string.

The first instinct is one big class that checks the language and calls a matching private method:

LeetCodeCompiler (naive)
class LeetCodeCompiler {
  public void compile(String code, String language) {
    if      (language.equals("JAVA"))   compileJava(code);
    else if (language.equals("CPP"))    compileCpp(code);
    else if (language.equals("PYTHON")) compilePython(code);
  }
  private void compileJava(String code)   { /* ... */ }
  private void compileCpp(String code)    { /* ... */ }
  private void compilePython(String code) { /* ... */ }
}
+ Correction to the source code: the original branches read loggerMedium.equals("CPP") in the C++ and Python checks, that's a copy-paste bug from a different (logger) example. It should compare the language parameter, as written above.

It works, but ask the natural question: what happens when we add C#? I have to crack open this class, add another else if, add another private method, and re-test the whole thing. Every new language modifies the one class everyone depends on.

Trade-offs Pro: trivial to read for 2-3 cases. Cons: the class grows forever, mixes "which language" routing with "how to compile" logic, and every change touches shared code. It violates the Single Responsibility and Open/Closed principles.
+ Open/Closed Principle (OCP): software should be open for extension but closed for modification, you should add behavior by adding code, not by editing existing, working code. The naive version fails this: extending it means editing it.

5. Solution 2, the "OK" approach (extract classes) #

Pull each language into its own class, but keep the if/else routing in the client.

Better: give each language a dedicated class and have the main compiler hold instances of all of them.

LeetCodeCompiler (OK)
class LeetCodeCompiler {
  private JavaCompiler   jCompiler;
  private CppCompiler    cppCompiler;
  private PythonCompiler pCompiler;

  public LeetCodeCompiler() {           // builds all 3 up front
    jCompiler   = new JavaCompiler();
    cppCompiler = new CppCompiler();
    pCompiler   = new PythonCompiler();
  }

  public void compile(String code, String language) {
    if      (language.equals("JAVA"))   jCompiler.compileJava(code);
    else if (language.equals("CPP"))    cppCompiler.compileCpp(code);
    else if (language.equals("PYTHON")) pCompiler.compilePython(code);
  }
}

Now the compilation logic lives in separate, focused classes, that's real progress. But two problems remain. First, the client still knows about every concrete class and still has the if/else chain, so adding C# still edits this class. Second, each concrete class has a differently-named method (compileJava, compileCpp...), so they aren't interchangeable, I can't treat them uniformly.

Pitfall The constructor eagerly news-up all three compilers even if I only ever use one. Worse, the lack of a shared interface means I can't loop over them or swap them, the very thing the next step fixes.

6. Solution 3, the Factory approach #

Introduce a common interface, then let a factory decide which concrete class to build, so the client never says new and never branches on language.

Two moves make this click. Move 1: define a single interface Compilation with one uniform method compile(String code), and make every language class implement it. Now all compilers are interchangeable. Move 2: create a CompilationFactory whose job is to take the language string and hand back the right Compilation object. The client just asks the factory and calls compile.

factory solution (essence)
interface Compilation { void compile(String code); }

class CompilationFactory {
  public static Compilation getInstance(String language) {
    switch (language) {
      case "JAVA":   return new JavaCompiler();
      case "PYTHON": return new PythonCompiler();
      case "CPP":    return new CppCompiler();
      default:       return null;
    }
  }
}

class LeetCodeCompiler {           // the Client
  public void compile(String code, String language) {
    Compilation instance = CompilationFactory.getInstance(language);
    instance.compile(code);     // no `new`, no if/else here
  }
}

The payoff shows when C# arrives: I write a CSharpCompiler implements Compilation and add one case to the factory. The client class doesn't change at all. Creation is centralized in one place, and the rest of the app talks only to the interface.

Why it matters This is the core lesson: delegate object creation instead of scattering new everywhere, and choose the concrete type dynamically at runtime based on input. The client depends on an abstraction, not on concrete classes, which is the Dependency Inversion idea in action.
+ Honest caveat on OCP: this design still edits the factory's switch when a new type is added, so the factory itself isn't fully closed for modification. That's a known, accepted trade-off, the leak is isolated to one tiny place. A registry map (Map <String, Supplier<Compilation>> ) can remove even that, letting new types self-register. See the lab challenge. ↓ Exercise 6

7. The four roles (UML structure) #

Every Factory setup has four named roles, learn these and you can map the pattern onto anything.

RoleWhat it isIn the compiler example
ProductThe interface or abstract class the factory returns. The contract. Compilation
ConcreteProductThe real classes that implement/extend Product. JavaCompiler, CppCompiler, PythonCompiler
Creator / ProductFactoryHolds the factory method that builds and returns a Product. CompilationFactory.getInstance()
ClientNeeds a Product but asks the Factory for it instead of new-ing one. LeetCodeCompiler
                        FACTORY METHOD — role map
                        Client ───asks for──▶ ProductFactory
     │                        │ creates
     │ uses                   ▼
     ▼                   ┌──────────┐
   Product (interface)◀──│  new Xxx │
     ▲                   └──────────┘
     │ implements
   ┌───────────┬───────────┬───────────┐
 ConcreteA   ConcreteB   ConcreteC   ...
doodle: Client → [Factory] → returns a shape; 3 shapes (□○△) sit under a "Product" umbrella line
                    
Interview angle A favorite question is "what's the difference between Simple Factory, Factory Method, and Abstract Factory?" Simple Factory = one factory class with a switch (what's shown here). Factory Method (GoF) = a method in a base class that subclasses override to decide the product, creation via inheritance. Abstract Factory = a factory of factories that creates whole families of related products. +

8. Practice mapping, the WhatsApp Status feature #

WhatsApp lets a user post a status visible for 24 hours, and the content can be Text, Image, or Video, perfect for Factory.

The exercise is to map this onto the four roles. A status type is chosen at runtime based on what the user posts, exactly the "pick a concrete type dynamically" situation Factory is built for.

RoleWhatsApp Status mapping
Product Status interface, e.g. void display()
ConcreteProduct TextStatus, ImageStatus, VideoStatus
ProductFactory StatusFactory.getStatus(type)
ClientThe status-posting feature that asks the factory and renders the result

The full runnable version is in the code section. See WhatsApp Status code ↓ and a hands-on build in Exercise 5.

Why it matters The same four-role skeleton drops onto wildly different domains, compilers, notifications, payment methods, document exporters, UI buttons per OS. Once I can name the roles, I can apply the pattern anywhere objects of a varying type must be created from input.

Key Takeaways#

  1. A design pattern is a proven, reusable template for a recurring design problem, a cookbook recipe, not finished code.
  2. The three GoF families are Creational, Structural, Behavioral; Factory Method is Creational. +
  3. The Factory pattern delegates object creation to a dedicated component and picks the concrete type at runtime from input.
  4. The client codes to an interface (Compilation ) and never calls new on concrete classes, that's the loose coupling that makes it valuable.
  5. Four roles: Product, ConcreteProduct, ProductFactory (Creator), Client.
  6. Adding a new type means add a class + one case, the client stays untouched, satisfying OCP (modulo the small switch leak, fixable with a registry).
  7. Solution ladder: naive if/else → extracted classes → factory; each step reduces coupling.
  8. Know the trio: Simple Factory (switch) vs Factory Method (subclass override) vs Abstract Factory (families). +
III.

Code Samples  (runnable, Java 17+) #

Standard JDK only. Each file is self-contained, compile with javac File.java and run with java ClassName. The compileX bodies just print a line so you can see the dispatch working.

3.1 Naive if/else  (maps to Notes §4) #

● ● ● NaiveCompiler.java javac NaiveCompiler.java && java NaiveCompiler
public class NaiveCompiler {
  // One class, one big branch. Works, but every new language edits this method.
  public void compile(String code, String language) {
    if      (language.equals("JAVA"))   compileJava(code);
    else if (language.equals("CPP"))    compileCpp(code);   // fixed: was checking the wrong var
    else if (language.equals("PYTHON")) compilePython(code);
    else System.out.println("Unsupported: " + language);
  }

  private void compileJava(String c)   { System.out.println("Compiling Java: " + c); }
  private void compileCpp(String c)    { System.out.println("Compiling C++:  " + c); }
  private void compilePython(String c) { System.out.println("Compiling Py:   " + c); }

  public static void main(String[] args) {
    NaiveCompiler nc = new NaiveCompiler();
    nc.compile("x=1", "JAVA");
    nc.compile("y=2", "PYTHON");
    nc.compile("z=3", "GO");     // not supported → handled by else
  }
}
Expected output
Compiling Java: x=1
Compiling Py:   y=2
Unsupported: GO

Why this behaves so: dispatch is decided by string equality at the call site. Adding "GO" support means returning here and editing the chain, the smell the factory removes.

3.2 Factory solution  (maps to Notes §6 &§7) #

● ● ● FactoryDemo.java javac FactoryDemo.java && java FactoryDemo
// ---- Product: the contract every compiler obeys ----
interface Compilation { void compile(String code); }

// ---- ConcreteProducts: interchangeable because the method name is uniform ----
class JavaCompiler implements Compilation {
  public void compile(String c) { System.out.println("[Java]   " + c); }
}
class CppCompiler implements Compilation {
  public void compile(String c) { System.out.println("[C++]    " + c); }
}
class PythonCompiler implements Compilation {
  public void compile(String c) { System.out.println("[Python] " + c); }
}

// ---- ProductFactory: the ONLY place that knows the concrete classes ----
class CompilationFactory {
  public static Compilation getInstance(String language) {
    switch (language) {                       // new lang → add ONE case here
      case "JAVA":   return new JavaCompiler();
      case "PYTHON": return new PythonCompiler();
      case "CPP":    return new CppCompiler();
      default: throw new IllegalArgumentException("Unknown: " + language);
    }
  }
}

// ---- Client: depends only on the interface; never says `new` ----
public class FactoryDemo {
  public void compile(String code, String language) {
    Compilation instance = CompilationFactory.getInstance(language);
    instance.compile(code);
  }
  public static void main(String[] args) {
    FactoryDemo client = new FactoryDemo();
    for (String lang : new String[]{"JAVA", "PYTHON", "CPP"})
      client.compile("print(1)", lang);
  }
}
Expected output
[Java]   print(1)
[Python] print(1)
[C++]    print(1)

Why this behaves so: the client variable's declared type is Compilation (the interface) but its runtime type is whatever the factory built, this is polymorphic dispatch. The for loop proves all three are interchangeable through one variable.

3.3 WhatsApp Status  (maps to Notes §8) #

● ● ● StatusDemo.java javac StatusDemo.java && java StatusDemo
// Product
interface Status { void display(); }

// ConcreteProducts — each status type knows how to render itself
class TextStatus implements Status {
  private final String text;
  TextStatus(String t) { this.text = t; }
  public void display() { System.out.println("📝 Text status: " + text); }
}
class ImageStatus implements Status {
  public void display() { System.out.println("🖼  Image status rendered"); }
}
class VideoStatus implements Status {
  public void display() { System.out.println("🎬 Video status (24h) playing"); }
}

// ProductFactory
class StatusFactory {
  public static Status getStatus(String type, String payload) {
    switch (type.toUpperCase()) {
      case "TEXT":  return new TextStatus(payload);
      case "IMAGE": return new ImageStatus();
      case "VIDEO": return new VideoStatus();
      default: throw new IllegalArgumentException("Bad type: " + type);
    }
  }
}

// Client — the status feature
public class StatusDemo {
  public static void main(String[] args) {
    StatusFactory.getStatus("text",  "Hello!").display();
    StatusFactory.getStatus("image", null).display();
    StatusFactory.getStatus("video", null).display();
  }
}
Expected output
📝 Text status: Hello!
🖼  Image status rendered
🎬 Video status (24h) playing

Why this behaves so: the factory is case-insensitive (toUpperCase() ) so "text" and "TEXT" both work, a small robustness touch. Same four roles as the compiler, different domain.

3.4 + Registry-based factory  (removes the switch, closes OCP, maps to Notes §6 caveat) #

● ● ● RegistryFactory.java javac RegistryFactory.java && java RegistryFactory
import java.util.*;
import java.util.function.*;

interface Compilation { void compile(String code); }

public class RegistryFactory {
  // language → "how to build it". Adding a type = put() one entry; no switch to edit.
  private static final Map<String, Supplier<Compilation>> REG = new HashMap<>();

  public static void register(String key, Supplier<Compilation> maker) {
    REG.put(key, maker);
  }

  public static Compilation getInstance(String language) {
    Supplier<Compilation> s = REG.get(language);
    if (s == null) throw new IllegalArgumentException("Unknown: " + language);
    return s.get();
  }

  public static void main(String[] args) {
    register("JAVA",   () -> c -> System.out.println("[Java] " + c));
    register("PYTHON", () -> c -> System.out.println("[Py]   " + c));
    // extension WITHOUT editing the factory body:
    register("CSHARP", () -> c -> System.out.println("[C#]   " + c));

    getInstance("JAVA").compile("a()");
    getInstance("CSHARP").compile("b()");
  }
}
Expected output
[Java] a()
[C#]   b()

Why this behaves so: a Supplier<Compilation> is a deferred constructor, calling .get() builds a fresh object on demand. New types register themselves into the map, so getInstance never changes, fully satisfying Open/Closed.

IV.

Hands-On Lab  (practice) #

▶ One-time Setup

1. Check the JDK is installed (need 11+, examples target 17):

terminal
java -version      # should print 11.x or higher
javac -version     # the compiler must also be present

2. Make a working folder:

terminal
mkdir factory-lab && cd factory-lab

3. Compile & run pattern (one public class per file, file name = class name):

terminal
javac FactoryDemo.java     # produces .class files
java FactoryDemo           # run, no .class extension

Tip: if you keep several top-level classes in one file, only the public one's name needs to match the filename. Everything here uses the standard JDK only, no Maven/Gradle needed.

Warm-up · 1

Run the naive version & feel the pain#

Goal: see why if/else routing doesn't scale.

  1. Copy NaiveCompiler.java (§3.1) into your folder.
  2. Compile and run it.
  3. Now add support for "GO" so it prints Compiling Go: ....
Predict, then verify: how many distinct places in the class did you have to touch to add Go? What does that tell you about adding the 10th language?
▶ Solution
NaiveCompiler.java (additions)
public void compile(String code, String language) {
    // ... existing branches ...
    else if (language.equals("GO")) compileGo(code);   // (1) new branch
}
private void compileGo(String c) {                     // (2) new method
    System.out.println("Compiling Go: " + c);
}

You edited the routing branch AND added a method, two edits inside a shared class, exactly the OCP violation. Repeat ten times and the class becomes a dumping ground.

Warm-up · 2

Define the Product interface#

Goal: make the compilers interchangeable by giving them one contract.

  1. Create Compilation with a single method void compile(String code).
  2. Make JavaCompiler and PythonCompiler implement it, each printing a distinct line.
  3. In main, store both in a Compilation[] and loop, calling compile on each.
Predict, then verify: can you put two different concrete classes in the same array? Why does that work now but wouldn't have in the "OK" solution from §5?
▶ Solution
Ex2.java
interface Compilation { void compile(String code); }

class JavaCompiler implements Compilation {
  public void compile(String c) { System.out.println("[Java] " + c); }
}
class PythonCompiler implements Compilation {
  public void compile(String c) { System.out.println("[Py]   " + c); }
}
public class Ex2 {
  public static void main(String[] a) {
    Compilation[] all = { new JavaCompiler(), new PythonCompiler() };
    for (Compilation c : all) c.compile("x");   // uniform call
  }
}
Expected output
[Java] x
[Py]   x

It works because both share the Compilation type. In §5 each class had a uniquely-named method, so they couldn't be treated uniformly.

Core · 3

Build the factory#

Goal: centralize creation so the client stops using new.

  1. Add a CompilationFactory with static Compilation getInstance(String language) using a switch over JAVA/PYTHON/CPP.
  2. Write a client that calls getInstance(language).compile(code), with no new and no if/else in the client.
  3. Throw IllegalArgumentException for unknown languages.
Predict, then verify: what is the declared type of the variable the client holds, and what is its runtime type? Why is that gap the whole point of the pattern?
▶ Solution

This is the full FactoryDemo.java (§3.2). The key client line:

client core
Compilation instance = CompilationFactory.getInstance(language);
instance.compile(code);   // declared type Compilation, runtime type JavaCompiler/etc.

Declared type = Compilation (interface); runtime type = whatever the factory built. The client is blind to the concrete class, which is precisely the decoupling Factory delivers.

Core · 4

Extend without touching the client#

Goal: prove the payoff, add C# and confirm the client is untouched.

  1. Add CSharpCompiler implements Compilation.
  2. Add one case "CSHARP" to the factory.
  3. Run the client with "CSHARP", without editing the client class at all.
Predict, then verify: before running, list every file/class you expect to modify. Was the client class on your list? Should it be?
▶ Solution
additions only
class CSharpCompiler implements Compilation {  // new class
  public void compile(String c) { System.out.println("[C#] " + c); }
}
// inside CompilationFactory.getInstance switch:
case "CSHARP": return new CSharpCompiler();    // one new case

Two additions, the client class is NOT one of them. That untouched client is the measurable benefit of the pattern.

Core · 5

Apply it to WhatsApp Status#

Goal: transfer the four roles to a new domain.

  1. Define Status with void display().
  2. Implement TextStatus (stores text), ImageStatus, VideoStatus.
  3. Write StatusFactory.getStatus(type, payload) and a client that posts one of each.
  4. Name which class plays each role: Product, ConcreteProduct, ProductFactory, Client.
Predict, then verify: which role does the main method play, and which does Status play? Map all four before peeking.
▶ Solution

Full code in StatusDemo.java (§3.3). Role map:

RoleClass
Product Status
ConcreteProduct TextStatus, ImageStatus, VideoStatus
ProductFactory StatusFactory
Client StatusDemo.main
Challenge · 6

Kill the switch with a registry#

Goal: make the factory fully Open/Closed, no edits to getInstance when adding types.

  1. Replace the switch with a Map <String, Supplier<Compilation>>.
  2. Add a register(key, supplier) method.
  3. Register JAVA, PYTHON, and a brand-new type via register, then resolve and run them.
  4. Confirm you never edited getInstance 's body to add the new type.
Predict, then verify: why does the map store a Supplier<Compilation> instead of a Compilation directly? What would break if you stored ready-made instances and the products were stateful?
▶ Solution

Full code in RegistryFactory.java (§3.4).

A Supplier defers construction so each getInstance call yields a fresh object, important if compilers hold per-job state. Storing ready instances would share one mutable object across all callers, risking cross-contamination. The map approach means new types call register and getInstance never changes, true OCP.


Self-check rubric, how to know you've got it#

  • Roles: I can point at any factory codebase and label Product / ConcreteProduct / ProductFactory / Client without hesitating.
  • The "no new" rule: I can explain why the client holding an interface type while the factory does the new is what creates loose coupling.
  • Extension test: I can add a new product type touching only (a) a new class and (b) the factory, never the client.
  • Trade-off honesty: I can state where the simple switch-factory still violates OCP and how a registry fixes it.
  • Taxonomy: I can distinguish Simple Factory vs Factory Method vs Abstract Factory in one sentence each.
  • Transfer: I mapped the pattern onto a second domain (WhatsApp Status) without re-reading the compiler example.
My personal study notes · Factory Method Design Pattern · Java · [+] marks my own additions & corrections beyond the source.