Factory Method Pattern: Creational Design Guide
TL;DR
The Factory Method Pattern defines an interface for creating objects but lets subclasses decide which class to instantiate. Instead of calling constructors directly, you call a factory method that returns objects, making your code more flexible and easier to extend without modifying existing code.
Core Concept
What is the Factory Method Pattern?
The Factory Method Pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. Instead of using direct constructor calls (new ClassName()), you call a factory method that returns an object.
Why Use Factory Method?
Direct instantiation creates tight coupling between your code and concrete classes. When you write product = ConcreteProduct(), you’re locked into that specific class. If requirements change and you need a different product type, you must modify existing code.
The Factory Method solves this by:
- Decoupling creation from usage: Client code doesn’t need to know which concrete class it’s using
- Supporting extensibility: Add new product types without changing existing code
- Centralizing object creation logic: Changes to instantiation logic happen in one place
Core Components
Product Interface: Defines the interface that all concrete products must implement.
Concrete Products: Specific implementations of the product interface.
Creator (Abstract): Declares the factory method that returns a Product object. May provide a default implementation.
Concrete Creators: Override the factory method to return different concrete products.
When to Use It
Use Factory Method when:
- You don’t know beforehand the exact types of objects your code will work with
- You want to provide users of your library a way to extend internal components
- You want to save system resources by reusing existing objects instead of rebuilding them
- Your object creation involves complex logic that shouldn’t be repeated
Real-World Analogy
Think of a logistics company. The Logistics class has a createTransport() factory method. RoadLogistics creates Truck objects, while SeaLogistics creates Ship objects. Both trucks and ships implement the Transport interface, so the delivery planning code works with any transport type without knowing the specifics.
Visual Guide
Factory Method Pattern Structure
classDiagram
class Creator {
<<abstract>>
+factoryMethod()* Product
+someOperation()
}
class ConcreteCreatorA {
+factoryMethod() Product
}
class ConcreteCreatorB {
+factoryMethod() Product
}
class Product {
<<interface>>
+operation()
}
class ConcreteProductA {
+operation()
}
class ConcreteProductB {
+operation()
}
Creator <|-- ConcreteCreatorA
Creator <|-- ConcreteCreatorB
Product <|.. ConcreteProductA
Product <|.. ConcreteProductB
Creator ..> Product
ConcreteCreatorA ..> ConcreteProductA : creates
ConcreteCreatorB ..> ConcreteProductB : creates
The Creator declares the factory method that returns Product objects. Concrete Creators override this method to instantiate specific Concrete Products.
Factory Method Flow
sequenceDiagram
participant Client
participant ConcreteCreator
participant ConcreteProduct
Client->>ConcreteCreator: someOperation()
ConcreteCreator->>ConcreteCreator: factoryMethod()
ConcreteCreator->>ConcreteProduct: new ConcreteProduct()
ConcreteProduct-->>ConcreteCreator: product instance
ConcreteCreator->>ConcreteProduct: operation()
ConcreteProduct-->>ConcreteCreator: result
ConcreteCreator-->>Client: result
Client calls a method on the Creator, which uses the factory method to create the appropriate product and then works with it through the Product interface.
Examples
Example 1: Document Creation System
Let’s build a document processing system where different applications create different document types.
from abc import ABC, abstractmethod
# Product Interface
class Document(ABC):
@abstractmethod
def open(self) -> str:
pass
@abstractmethod
def save(self, content: str) -> str:
pass
# Concrete Products
class PDFDocument(Document):
def open(self) -> str:
return "Opening PDF document with Adobe Reader"
def save(self, content: str) -> str:
return f"Saving '{content}' as PDF with compression"
class WordDocument(Document):
def open(self) -> str:
return "Opening Word document with Microsoft Word"
def save(self, content: str) -> str:
return f"Saving '{content}' as DOCX with formatting"
class TextDocument(Document):
def open(self) -> str:
return "Opening text document with default editor"
def save(self, content: str) -> str:
return f"Saving '{content}' as plain text"
# Creator (Abstract)
class Application(ABC):
@abstractmethod
def create_document(self) -> Document:
"""Factory method - subclasses override this"""
pass
def new_document(self, content: str) -> str:
"""Template method that uses the factory method"""
# This method works with Document interface, not concrete classes
doc = self.create_document()
open_msg = doc.open()
save_msg = doc.save(content)
return f"{open_msg}\n{save_msg}"
# Concrete Creators
class PDFApplication(Application):
def create_document(self) -> Document:
return PDFDocument()
class WordApplication(Application):
def create_document(self) -> Document:
return WordDocument()
class TextApplication(Application):
def create_document(self) -> Document:
return TextDocument()
# Client code
def client_code(app: Application, content: str):
"""Client doesn't know which concrete document it's working with"""
result = app.new_document(content)
print(result)
print()
# Usage
if __name__ == "__main__":
print("PDF Application:")
client_code(PDFApplication(), "Annual Report 2024")
print("Word Application:")
client_code(WordApplication(), "Meeting Notes")
print("Text Application:")
client_code(TextApplication(), "Quick Note")
Expected Output:
PDF Application:
Opening PDF document with Adobe Reader
Saving 'Annual Report 2024' as PDF with compression
Word Application:
Opening Word document with Microsoft Word
Saving 'Meeting Notes' as DOCX with formatting
Text Application:
Opening text document with default editor
Saving 'Quick Note' as plain text
Key Points:
Application.new_document()works with theDocumentinterface, never knowing the concrete type- Each concrete application creates its specific document type
- Adding a new document type (e.g.,
MarkdownDocument) requires only adding new classes, not modifying existing code
Try it yourself: Add a SpreadsheetDocument class and SpreadsheetApplication class. The spreadsheet should save with “cell formatting” in its message.
Example 2: Payment Processing System
A real-world e-commerce scenario where different regions use different payment processors.
from abc import ABC, abstractmethod
from typing import Dict
# Product Interface
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> Dict[str, any]:
pass
@abstractmethod
def refund(self, transaction_id: str, amount: float) -> Dict[str, any]:
pass
# Concrete Products
class StripeProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> Dict[str, any]:
return {
"status": "success",
"transaction_id": f"stripe_{amount}_txn",
"amount": amount,
"fee": amount * 0.029 + 0.30, # Stripe's fee structure
"processor": "Stripe"
}
def refund(self, transaction_id: str, amount: float) -> Dict[str, any]:
return {
"status": "refunded",
"transaction_id": transaction_id,
"amount": amount,
"processor": "Stripe"
}
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> Dict[str, any]:
return {
"status": "success",
"transaction_id": f"paypal_{amount}_txn",
"amount": amount,
"fee": amount * 0.034 + 0.30, # PayPal's fee structure
"processor": "PayPal"
}
def refund(self, transaction_id: str, amount: float) -> Dict[str, any]:
return {
"status": "refunded",
"transaction_id": transaction_id,
"amount": amount,
"processor": "PayPal"
}
class LocalBankProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> Dict[str, any]:
return {
"status": "success",
"transaction_id": f"bank_{amount}_txn",
"amount": amount,
"fee": amount * 0.01, # Lower fee for local bank
"processor": "Local Bank"
}
def refund(self, transaction_id: str, amount: float) -> Dict[str, any]:
return {
"status": "refunded",
"transaction_id": transaction_id,
"amount": amount,
"processor": "Local Bank"
}
# Creator (Abstract)
class PaymentService(ABC):
@abstractmethod
def create_processor(self) -> PaymentProcessor:
"""Factory method"""
pass
def checkout(self, amount: float) -> Dict[str, any]:
"""Business logic that uses the factory method"""
processor = self.create_processor()
result = processor.process_payment(amount)
# Add business logic here
net_amount = result["amount"] - result["fee"]
result["net_amount"] = round(net_amount, 2)
return result
def process_refund(self, transaction_id: str, amount: float) -> Dict[str, any]:
processor = self.create_processor()
return processor.refund(transaction_id, amount)
# Concrete Creators
class USPaymentService(PaymentService):
def create_processor(self) -> PaymentProcessor:
return StripeProcessor()
class EUPaymentService(PaymentService):
def create_processor(self) -> PaymentProcessor:
return PayPalProcessor()
class AsiaPaymentService(PaymentService):
def create_processor(self) -> PaymentProcessor:
return LocalBankProcessor()
# Client code
def process_order(service: PaymentService, amount: float):
print(f"Processing ${amount} payment...")
result = service.checkout(amount)
print(f"Processor: {result['processor']}")
print(f"Transaction ID: {result['transaction_id']}")
print(f"Fee: ${result['fee']:.2f}")
print(f"Net Amount: ${result['net_amount']:.2f}")
print()
if __name__ == "__main__":
amount = 100.00
print("US Customer:")
process_order(USPaymentService(), amount)
print("EU Customer:")
process_order(EUPaymentService(), amount)
print("Asia Customer:")
process_order(AsiaPaymentService(), amount)
Expected Output:
US Customer:
Processing $100.0 payment...
Processor: Stripe
Transaction ID: stripe_100.0_txn
Fee: $3.20
Net Amount: $96.80
EU Customer:
Processing $100.0 payment...
Processor: PayPal
Transaction ID: paypal_100.0_txn
Fee: $3.70
Net Amount: $96.30
Asia Customer:
Processing $100.0 payment...
Processor: Local Bank
Transaction ID: bank_100.0_txn
Fee: $1.00
Net Amount: $99.00
Key Points:
- The
checkout()method contains business logic that works with anyPaymentProcessor - Different regions get different processors without changing the core payment logic
- Easy to add new regions or switch processors for existing regions
Java/C++ Notes:
- In Java, use
abstract classorinterfacefor the Creator and Product - In C++, use pure virtual functions (
virtual ReturnType method() = 0;) - Both languages require explicit interface implementation/inheritance declarations
Try it yourself: Add a CryptoProcessor that charges a 1.5% fee and create a CryptoPaymentService. Test it with a $100 payment.
Common Mistakes
1. Confusing Factory Method with Simple Factory
Mistake: Creating a single class with a method that returns different objects based on parameters.
# This is NOT Factory Method Pattern - it's Simple Factory
class DocumentFactory:
@staticmethod
def create_document(doc_type: str) -> Document:
if doc_type == "pdf":
return PDFDocument()
elif doc_type == "word":
return WordDocument()
# Adding new types requires modifying this method
Why it’s wrong: Simple Factory uses conditional logic in one place. Factory Method uses inheritance and polymorphism. The Factory Method Pattern has no conditional statements—the subclass type determines which object gets created.
Correct approach: Use inheritance where each creator subclass creates its specific product type.
2. Putting Too Much Logic in the Factory Method
Mistake: Making the factory method do more than just instantiation.
class BadApplication(Application):
def create_document(self) -> Document:
doc = PDFDocument()
doc.open() # Don't do this here!
doc.set_author("John Doe") # Configuration belongs elsewhere
doc.validate() # Business logic doesn't belong here
return doc
Why it’s wrong: The factory method should focus on object creation. Additional configuration and business logic should happen in the calling code or in separate methods.
Correct approach: Keep factory methods simple—just instantiate and return.
class GoodApplication(Application):
def create_document(self) -> Document:
return PDFDocument() # Just create and return
def new_document(self, author: str) -> Document:
doc = self.create_document()
doc.set_author(author) # Configure after creation
return doc
3. Not Using the Product Interface Consistently
Mistake: Casting the product back to a concrete type, defeating the purpose of the pattern.
def bad_client_code(app: Application):
doc = app.create_document()
# Don't do this!
if isinstance(doc, PDFDocument):
doc.set_compression_level(9) # Accessing PDF-specific method
elif isinstance(doc, WordDocument):
doc.enable_track_changes() # Accessing Word-specific method
Why it’s wrong: You’re checking concrete types, which creates tight coupling. The whole point is to work through the interface.
Correct approach: If you need type-specific behavior, add it to the Product interface or use a different pattern (like Strategy).
4. Creating Unnecessary Inheritance Hierarchies
Mistake: Using Factory Method when a simple constructor would suffice.
# Overkill for simple cases
class SimpleDataHolder:
def __init__(self, data):
self.data = data
# Don't need a factory for this!
class SimpleDataHolderFactory(ABC):
@abstractmethod
def create_holder(self) -> SimpleDataHolder:
pass
Why it’s wrong: Factory Method adds complexity. Use it when you need the flexibility it provides, not as a default.
When to use it: When you have multiple related types, when creation logic might change, or when you need to decouple creation from usage.
5. Forgetting to Make the Creator Abstract
Mistake: Making the creator concrete when it should be abstract, allowing direct instantiation.
class Application: # Should be ABC
def create_document(self) -> Document:
# No implementation or default implementation
return Document() # Problematic if Document is abstract
Why it’s wrong: If the creator isn’t abstract, clients might instantiate it directly, bypassing the factory method’s purpose.
Correct approach: Make the creator abstract unless you have a sensible default implementation.
class Application(ABC):
@abstractmethod
def create_document(self) -> Document:
pass
Interview Tips
1. Start with the Problem, Not the Pattern
When asked about Factory Method, don’t immediately jump into UML diagrams. Start by explaining the problem it solves:
Good answer: “Factory Method solves the problem of tight coupling between code and concrete classes. When you instantiate objects directly with constructors, you’re locked into specific implementations. This pattern lets you defer the instantiation decision to subclasses, making your code more flexible and extensible.”
Then provide a concrete example from your experience or a common scenario.
2. Know How to Distinguish It from Other Creational Patterns
Interviewers often ask: “How is Factory Method different from Abstract Factory or Builder?”
Be ready to explain:
- Factory Method: Creates one product type through inheritance. Each creator subclass creates its variant.
- Abstract Factory: Creates families of related products. One factory creates multiple related objects.
- Builder: Constructs complex objects step-by-step. Focuses on the construction process.
- Simple Factory: Not a GoF pattern. Uses conditional logic in one method, not inheritance.
Example answer: “Factory Method uses inheritance—each subclass decides what to create. Abstract Factory uses composition—one factory object creates multiple related products. If I need to create just documents with different variants, Factory Method works. If I need to create documents AND their associated templates AND their exporters as a family, I’d use Abstract Factory.”
3. Discuss Real-World Trade-offs
Interviewers want to know you understand when NOT to use a pattern.
Be prepared to say:
- “Factory Method adds complexity through additional classes. For simple cases, direct instantiation is clearer.”
- “It’s most valuable when you expect to add new product types frequently without modifying existing code.”
- “The pattern works best when products share a common interface and clients can work through that interface.”
Red flag answer: “I always use Factory Method for object creation.” This shows you apply patterns blindly.
4. Code It Quickly in an Interview Setting
Practice implementing a basic Factory Method in 5-10 minutes. Common interview scenarios:
Scenario 1: “Design a notification system that sends messages via Email, SMS, or Push notifications.”
Scenario 2: “Create a payment processing system supporting multiple payment gateways.”
Scenario 3: “Build a logging system with different log destinations (File, Database, Cloud).”
Quick implementation checklist:
- Define the Product interface (2-3 methods)
- Create 2-3 Concrete Products
- Define the abstract Creator with the factory method
- Create 2-3 Concrete Creators
- Show client code that works through the interface
5. Connect to SOLID Principles
Factory Method exemplifies several SOLID principles. Mention this to show deeper understanding:
Open/Closed Principle: “You can add new product types by creating new subclasses without modifying existing creator code.”
Dependency Inversion Principle: “High-level code depends on the Product interface, not concrete implementations.”
Single Responsibility Principle: “Object creation logic is separated from business logic.”
6. Prepare for Follow-Up Questions
Common follow-ups:
Q: “What if you need to create multiple related objects?” A: “That’s when I’d consider Abstract Factory, which creates families of related objects.”
Q: “How do you handle complex object construction?” A: “Factory Method handles which class to instantiate. For complex construction steps, I’d combine it with Builder pattern.”
Q: “Can you use Factory Method with dependency injection?” A: “Yes, you can inject the creator, and the factory method determines which concrete product gets created. This combines the benefits of both patterns.”
7. Show You Can Refactor to the Pattern
Interviewers might give you code with direct instantiation and ask you to refactor it. Practice identifying:
- Multiple places where similar objects are created
- Conditional logic based on types
- Code that would benefit from adding new types without modification
Example: “I see this code has if type == 'A': return ClassA() in multiple places. I’d refactor this to Factory Method by creating an abstract creator with concrete creators for each type, eliminating the conditionals and centralizing creation logic.”
Key Takeaways
-
Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate, promoting loose coupling between client code and concrete classes through polymorphism and inheritance.
-
Use Factory Method when you need flexibility in object creation, especially when you expect to add new product types frequently or when creation logic might vary across different contexts without modifying existing code.
-
The pattern consists of four key components: Product interface, Concrete Products, abstract Creator with factory method, and Concrete Creators that override the factory method to instantiate specific products.
-
Keep factory methods simple—they should only instantiate and return objects, not perform business logic or complex configuration. Additional setup should happen in separate methods or calling code.
-
Factory Method exemplifies SOLID principles, particularly Open/Closed (extensible without modification) and Dependency Inversion (depend on abstractions, not concretions), making it a fundamental pattern for maintainable object-oriented design.