Factory Design Pattern: Simple Factory, Factory Method, and Abstract Factory
The Factory Design Pattern is one of the most widely used creational design patterns in low-level system design (LLD) and real-world software engineering. Its primary mission is to decouple the object creation logic from the core business logic, ensuring that client code remains extensible, maintainable, and free from direct dependencies on concrete classes (new operators).
1. Why Do We Need Factory Patterns?
In modern software applications, code is broadly split into two distinct responsibilities:
- Business Logic: The core domain workflows (e.g., executing notification pipelines, calculating discounts, processing payments).
- Object Creation Logic: Deciding which specific concrete instance to instantiate based on incoming flags, configuration, or environment variables.
The Problem: Tangled Creation and Business Logic
When object instantiation is scattered across business logic using conditional blocks (if-else or switch-case with the new keyword), several architectural problems arise:
- Tight Coupling: The consuming class becomes tightly coupled to every concrete implementation.
- Violation of Open/Closed Principle (OCP): Introducing a new type forces modifications directly inside existing business logic files.
- Code Duplication & Cognitive Overhead: Reading complex business logic cluttered with initialization computations makes code difficult to maintain and unit test.
Real-Life Analogy
In a physical car or food factory, the consumer does not assemble the engine, chassis, or ingredients themselves. They simply submit an order/specifications to the factory, and the factory returns a fully assembled, ready-to-use product. The client knows what the product does (interface), not how each individual nut, bolt, or ingredient was assembled (creation logic).
2. The Mental Model: Bridging Strategy and Factory Patterns
To understand object creation design, consider how it interacts with patterns like the Strategy Pattern.
In the Strategy Pattern (e.g., a Robot having different behaviors like Talkable, Walkable, and Flyable), we inject algorithms dynamically into a context class via constructor or setter injection (Dependency Injection).
The Strategy Pattern assumes that the concrete dependency already exists. However, somewhere in the codebase, the application must execute the new keyword to create that dependency.
3. The Three Flavors of Factory
The term "Factory" encompasses three distinct architectural concepts:
4. Simple Factory (The Creational Idiom)
Concept & Mental Model
A Simple Factory removes direct new ConcreteClass() calls from the client by introducing a standalone class whose sole job is to instantiate objects based on an input parameter.
Structural Flow
Client
|
| 1. Requests product: createBurger("standard")
v
BurgerFactory
|
| 2. Evaluates type via if-else / switch
v
Instantiates new StandardBurger()
|
| 3. Returns Burger interface reference
v
Client executes burger.prepare()
Naive vs. Simple Factory Implementation (Java)
Product Hierarchy:
// Abstract Product Interface
public interface Burger {
void prepare();
}
// Concrete Products
public class BasicBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Basic Burger: Bun + Single Patty + Ketchup.");
}
}
public class StandardBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Standard Burger: Bun + Patty + Cheese + Lettuce.");
}
}
public class PremiumBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Premium Burger: Gourmet Bun + Double Patty + Special Sauce.");
}
}
Simple Factory Implementation:
public class SimpleBurgerFactory {
public Burger createBurger(String type) {
if (type == null) {
throw new IllegalArgumentException("Type cannot be null");
}
switch (type.toLowerCase()) {
case "basic":
return new BasicBurger();
case "standard":
return new StandardBurger();
case "premium":
return new PremiumBurger();
default:
throw new IllegalArgumentException("Invalid burger type: " + type);
}
}
}
Client Code:
public class Client {
public static void main(String[] args) {
SimpleBurgerFactory factory = new SimpleBurgerFactory();
// Client requests standard burger without invoking 'new StandardBurger()'
Burger burger = factory.createBurger("standard");
burger.prepare();
}
}
Note on OCP Limitation: While Simple Factory decouples the client from concrete classes, adding a new burger type still requires modifying the
SimpleBurgerFactoryclass (violating the Open/Closed Principle at the factory level). To achieve true extensibility, we evolve to the Factory Method Pattern.
5. Factory Method Pattern (GoF)
Concept & Mental Model
The Factory Method Pattern defines an interface/abstract class for creating an object, but lets subclasses decide which class to instantiate. It replaces direct factory logic with inheritance and polymorphism.
The Scenario: Expanding Franchise Types
Imagine two restaurant franchises with different product variations:
- Singh Burger: Specializes in standard, classic meat/veggie burgers.
- King Burger: Specializes exclusively in healthy, whole-wheat burgers.
Architecture & Inheritance Breakdown
Complete Code Walkthrough (Java)
1. Products
// Base Product
public interface Burger {
void prepare();
}
// Standard Product Line
public class BasicBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Basic Burger with regular bun and patty.");
}
}
public class StandardBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Standard Burger with regular bun, cheese, and veggies.");
}
}
public class PremiumBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Premium Gourmet Burger with artisan bun.");
}
}
// Wheat Product Line
public class BasicWheatBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Basic Wheat Burger with 100% whole wheat bun.");
}
}
public class StandardWheatBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Standard Wheat Burger with brown bun and organic greens.");
}
}
public class PremiumWheatBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Premium Wheat Burger with multi-grain bun and gourmet tofu patty.");
}
}
2. Creators (The Factory Method Hierarchy)
// Abstract Creator
public abstract class BurgerFactory {
// The Factory Method
public abstract Burger createBurger(String type);
// Business operations can also be executed here
public Burger orderBurger(String type) {
Burger burger = createBurger(type);
burger.prepare();
return burger;
}
}
// Concrete Creator 1: Singh Burger (Classic Range)
public class SinghBurgerFactory extends BurgerFactory {
@Override
public Burger createBurger(String type) {
switch (type.toLowerCase()) {
case "basic":
return new BasicBurger();
case "standard":
return new StandardBurger();
case "premium":
return new PremiumBurger();
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
}
}
// Concrete Creator 2: King Burger (Wheat Range)
public class KingBurgerFactory extends BurgerFactory {
@Override
public Burger createBurger(String type) {
switch (type.toLowerCase()) {
case "basic":
return new BasicWheatBurger();
case "standard":
return new StandardWheatBurger();
case "premium":
return new PremiumWheatBurger();
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
}
}
3. Client Execution
public class Client {
public static void main(String[] args) {
// Client decides the franchise type dynamically
BurgerFactory franchise = new KingBurgerFactory();
// Factory handles instantiating the correct wheat variation
Burger myBurger = franchise.createBurger("basic");
myBurger.prepare();
}
}
6. Abstract Factory Pattern (GoF)
Concept & Mental Model
The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
When a franchise needs to supply multiple categories of products (e.g., both Burger AND GarlicBread) that must belong to the same product family (Classic vs. Whole Wheat), Abstract Factory enforces consistency across products.
Complete Code Walkthrough (Java)
1. Product Interfaces and Implementations
// Product Family 1: Burgers
public interface Burger {
void prepare();
}
public class ClassicBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Classic White-Flour Burger.");
}
}
public class WheatBurger implements Burger {
@Override
public void prepare() {
System.out.println("Preparing Healthy Whole-Wheat Burger.");
}
}
// Product Family 2: Garlic Breads
public interface GarlicBread {
void bake();
}
public class ClassicGarlicBread implements GarlicBread {
@Override
public void bake() {
System.out.println("Baking Classic Garlic Bread with white bread and butter.");
}
}
public class WheatGarlicBread implements GarlicBread {
@Override
public void bake() {
System.out.println("Baking Wheat Garlic Bread with multigrain baguette.");
}
}
2. Abstract Factory Interface & Concrete Factories
// Abstract Factory
public interface MealFactory {
Burger createBurger();
GarlicBread createGarlicBread();
}
// Concrete Factory 1: Classic Product Family
public class SinghMealFactory implements MealFactory {
@Override
public Burger createBurger() {
return new ClassicBurger();
}
@Override
public GarlicBread createGarlicBread() {
return new ClassicGarlicBread();
}
}
// Concrete Factory 2: Healthy Wheat Product Family
public class KingMealFactory implements MealFactory {
@Override
public Burger createBurger() {
return new WheatBurger();
}
@Override
public GarlicBread createGarlicBread() {
return new WheatGarlicBread();
}
}
3. Client Execution
public class Client {
public static void main(String[] args) {
// Configure factory family based on preference or runtime configuration
MealFactory mealFactory = new KingMealFactory();
Burger burger = mealFactory.createBurger();
GarlicBread bread = mealFactory.createGarlicBread();
burger.prepare(); // Output: Preparing Healthy Whole-Wheat Burger.
bread.bake(); // Output: Baking Wheat Garlic Bread with multigrain baguette.
}
}
7. Real-World Practical Example: Notification System
A common interview and production use case is the Multi-Channel Notification System.
Client
|
| 1. Requests channel ("SMS")
v
NotificationFactory
|
| 2. Instantiates concrete channel
v
SmsNotification (Implements Notification)
|
| 3. Returns instance
v
Client calls notification.sendNotification("Your OTP is 1234")
Production-Grade Notification Implementation
// Product Interface
public interface Notification {
void sendNotification(String recipient, String message);
}
// Concrete Implementations
public class EmailNotification implements Notification {
@Override
public void sendNotification(String recipient, String message) {
System.out.println("Connecting to SMTP Server... Sending Email to " + recipient + ": " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void sendNotification(String recipient, String message) {
System.out.println("Connecting to Telecom Gateway... Sending SMS to " + recipient + ": " + message);
}
}
public class PushNotification implements Notification {
@Override
public void sendNotification(String recipient, String message) {
System.out.println("Connecting to APNs/FCM... Sending Push to " + recipient + ": " + message);
}
}
// Notification Simple Factory
public class NotificationFactory {
public Notification createNotification(String channel) {
if (channel == null || channel.trim().isEmpty()) {
throw new IllegalArgumentException("Channel must not be empty");
}
switch (channel.toUpperCase()) {
case "EMAIL":
return new EmailNotification();
case "SMS":
return new SmsNotification();
case "PUSH":
return new PushNotification();
default:
throw new IllegalArgumentException("Unsupported channel: " + channel);
}
}
}
8. Strategy vs. Factory Pattern: Decision Framework
Engineering teams often face scenarios where both Strategy and Factory patterns seem viable (e.g., Notification processing, Payment systems). Use this decision framework to determine the right pattern:
Production Tip: In enterprise applications (e.g., Spring Boot), Strategy and Factory are often combined: a Factory instantiates or resolves the correct Strategy Bean, which is then executed by the client context.
9. Summary and Key Takeaways
- Decoupling Creational Concerns: The primary purpose of the Factory pattern family is to decouple business logic from object creation logic, isolating
newoperations. - Simple Factory: A non-GoF design idiom where a single concrete class handles instantiation via a conditional parameter. Fast to implement, but requires modification when adding new products.
- Factory Method: A GoF creational pattern that uses inheritance and polymorphism. The base creator defines the abstract creation method, and concrete subclasses decide which product class to instantiate.
- Abstract Factory: A GoF creational pattern providing an interface to create families of related products (e.g., Burgers and Garlic Breads) without coupling to concrete classes.
- Adherence to OCP: Factory Method and Abstract Factory honor the Open/Closed Principle by allowing new product lines to be added through new subclasses without modifying existing creation pipelines.
- Strategy vs. Factory: Choose Strategy when the core problem is dynamically varying algorithms on an existing object; choose Factory when the core problem is constructing objects without coupling to concrete classes.