Factory Method Design Pattern
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.
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
| Stage | What it does | Problem |
|---|---|---|
| Naive |
one class, if/else
on language, private compileX() methods
|
add C# = edit core class; not extensible |
| OK | separate JavaCompiler/CppCompiler... held as fields, still if/else | logic 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/else | Factory | |
|---|---|---|
| Coupling | tight (client knows all) | loose (knows interface) |
| Add new type | edit existing class | add class + 1 case |
| OCP | violated | mostly satisfied |
| Testing | hard to mock | easy (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
| Role | Maps to |
|---|---|
| Product |
Status
interface (e.g. display()
)
|
| ConcreteProduct |
TextStatus, ImageStatus, VideoStatus
|
| ProductFactory |
StatusFactory.getStatus(type)
|
| Client | WhatsApp 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
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.
| Family | Concerned with | Examples |
|---|---|---|
| Creational + | how objects get created | Factory Method, Abstract Factory, Singleton, Builder, Prototype |
| Structural + | how objects are composed into larger structures | Adapter, Decorator, Facade, Proxy, Composite |
| Behavioral + | how objects communicate & share responsibility | Observer, Strategy, Command, State, Iterator |
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.
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:
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:
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) { /* ... */ }
}
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.
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.
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.
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.
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.
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.
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.
| Role | What it is | In the compiler example |
|---|---|---|
| Product | The interface or abstract class the factory returns. The contract. |
Compilation
|
| ConcreteProduct | The real classes that implement/extend Product. |
JavaCompiler, CppCompiler, PythonCompiler
|
| Creator / ProductFactory | Holds the factory method that builds and returns a Product. |
CompilationFactory.getInstance()
|
| Client | Needs 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
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.
| Role | WhatsApp Status mapping |
|---|---|
| Product |
Status
interface, e.g. void display()
|
| ConcreteProduct |
TextStatus, ImageStatus, VideoStatus
|
| ProductFactory |
StatusFactory.getStatus(type)
|
| Client | The 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.
Key Takeaways
- A design pattern is a proven, reusable template for a recurring design problem, a cookbook recipe, not finished code.
- The three GoF families are Creational, Structural, Behavioral; Factory Method is Creational. +
- The Factory pattern delegates object creation to a dedicated component and picks the concrete type at runtime from input.
-
The client codes to an interface
(
Compilation) and never callsnewon concrete classes, that's the loose coupling that makes it valuable. - Four roles: Product, ConcreteProduct, ProductFactory (Creator), Client.
- 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).
- Solution ladder: naive if/else → extracted classes → factory; each step reduces coupling.
- Know the trio: Simple Factory (switch) vs Factory Method (subclass override) vs Abstract Factory (families). +
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)
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
}
}
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)
// ---- 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);
}
}
[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)
// 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();
}
}
📝 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)
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()");
}
}
[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.
Hands-On Lab (practice)
▶ One-time Setup
1. Check the JDK is installed (need 11+, examples target 17):
java -version # should print 11.x or higher
javac -version # the compiler must also be present
2. Make a working folder:
mkdir factory-lab && cd factory-lab
3. Compile & run pattern (one public class per file, file name = class name):
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.
Run the naive version & feel the pain
Goal: see why if/else routing doesn't scale.
- Copy NaiveCompiler.java (§3.1) into your folder.
- Compile and run it.
-
Now add support for
"GO"so it printsCompiling Go: ....
▶ Solution
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.
Define the Product interface
Goal: make the compilers interchangeable by giving them one contract.
-
Create
Compilationwith a single methodvoid compile(String code). -
Make
JavaCompilerandPythonCompilerimplement it, each printing a distinct line. -
In
main, store both in aCompilation[]and loop, callingcompileon each.
▶ Solution
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
}
}
[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.
Build the factory
Goal:
centralize creation so the client stops using new.
-
Add a
CompilationFactorywithstatic Compilation getInstance(String language)using a switch over JAVA/PYTHON/CPP. -
Write a client that calls
getInstance(language).compile(code), with nonewand no if/else in the client. -
Throw
IllegalArgumentExceptionfor unknown languages.
▶ Solution
This is the full FactoryDemo.java (§3.2). The key client line:
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.
Extend without touching the client
Goal: prove the payoff, add C# and confirm the client is untouched.
-
Add
CSharpCompiler implements Compilation. -
Add one
case "CSHARP"to the factory. -
Run the client with
"CSHARP", without editing the client class at all.
▶ Solution
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.
Apply it to WhatsApp Status
Goal: transfer the four roles to a new domain.
-
Define
Statuswithvoid display(). -
Implement
TextStatus(stores text),ImageStatus,VideoStatus. -
Write
StatusFactory.getStatus(type, payload)and a client that posts one of each. - Name which class plays each role: Product, ConcreteProduct, ProductFactory, Client.
main
method play, and which does Status
play? Map all four before peeking.
▶ Solution
Full code in StatusDemo.java (§3.3). Role map:
| Role | Class |
|---|---|
| Product |
Status
|
| ConcreteProduct |
TextStatus, ImageStatus, VideoStatus
|
| ProductFactory |
StatusFactory
|
| Client |
StatusDemo.main
|
Kill the switch with a registry
Goal:
make the factory fully Open/Closed, no edits to getInstance
when adding types.
-
Replace the switch with a
Map <String, Supplier<Compilation>>. -
Add a
register(key, supplier)method. -
Register JAVA, PYTHON, and a brand-new type via
register, then resolve and run them. -
Confirm you never edited
getInstance's body to add the new type.
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
newis 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.