Loading learning content...
Understanding a design pattern requires more than knowing what it does—you need to understand its structure: the participants, their roles, their relationships, and how they collaborate to achieve the pattern's goals.
The Factory Method pattern involves four key participants working together in a carefully designed relationship. In this page, we'll dissect each participant, understand why it exists, what responsibilities it carries, and how it interacts with the others. This structural understanding is essential for applying the pattern correctly in your own designs.
By the end of this page, you'll have a complete mental model of Factory Method's structure. You'll understand the Creator hierarchy, the Product hierarchy, how they connect through the factory method, and the collaboration dynamics that make the pattern work.
The Factory Method pattern consists of four participants that work together:
Let's examine each in detail.
This class diagram shows the fundamental structure. Notice the two parallel hierarchies:
The factory method in Creator returns Product. Each ConcreteCreator overrides it to return its matching ConcreteProduct.
The Product defines the interface for the objects that the factory method creates. This is the abstraction that both the Creator and client code depend upon.
1234567891011121314151617181920212223242526272829303132333435
// Product: The interface for objects the factory method creates// Can be an interface or abstract class // Example 1: Interface-based Productinterface Document { // Core operations all documents must support parse(): void; validate(): boolean; render(): string; // Properties readonly path: string; readonly format: DocumentFormat;} // Example 2: Abstract class-based Product (with shared behavior)abstract class Transport { protected passengers: number = 0; // Abstract methods - subclasses must implement abstract deliver(cargo: Cargo): void; abstract getCapacity(): number; // Concrete methods - shared behavior loadPassengers(count: number): void { if (count > this.getCapacity()) { throw new Error('Exceeds capacity'); } this.passengers = count; } getPassengerCount(): number { return this.passengers; }}Choose an interface when products only share a contract (method signatures). Choose an abstract class when products share implementation (code). In languages supporting multiple inheritance of interfaces (like Java, C#, TypeScript), prefer interfaces for maximum flexibility.
ConcreteProduct classes implement the Product interface. These are the actual objects that get created and used. Each ConcreteProduct provides its own implementation of the Product operations.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
// ConcreteProducts implement the Product interface // ConcreteProduct 1: PDF Documentclass PdfDocument implements Document { readonly format = DocumentFormat.PDF; private content: PdfContent | null = null; constructor(readonly path: string) {} parse(): void { // PDF-specific parsing using pdf.js or similar const buffer = readFileSync(this.path); this.content = PdfParser.parse(buffer); } validate(): boolean { if (!this.content) return false; return this.content.isValid() && this.content.hasRequiredMetadata(); } render(): string { if (!this.content) throw new Error('Document not parsed'); return this.content.toHtml(); }} // ConcreteProduct 2: Word Documentclass WordDocument implements Document { readonly format = DocumentFormat.WORD; private doc: WordContent | null = null; constructor(readonly path: string) {} parse(): void { // Word-specific parsing using docx library const zip = readWordFile(this.path); this.doc = WordParser.parse(zip); } validate(): boolean { if (!this.doc) return false; return this.doc.hasValidStructure(); } render(): string { if (!this.doc) throw new Error('Document not parsed'); return this.doc.toHtml(); }} // ConcreteProduct 3: Markdown Documentclass MarkdownDocument implements Document { readonly format = DocumentFormat.MARKDOWN; private text: string = ''; constructor(readonly path: string) {} parse(): void { // Simple text reading for markdown this.text = readFileSync(this.path, 'utf-8'); } validate(): boolean { // Markdown is always valid text return this.text.length > 0; } render(): string { // Convert markdown to HTML return marked(this.text); }}Notice how each ConcreteProduct is completely independent. PdfDocument knows about PDF parsing, WordDocument knows about Word parsing, but they share no code. The common behavior is defined by the interface, not by copying implementations. This is the essence of polymorphism.
The Creator is the class that declares the factory method. It may also define a default implementation of the factory method that returns a default ConcreteProduct type. The Creator's primary operations use the factory method to create Product objects.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
// Creator: Declares the factory method and uses it abstract class DocumentProcessor { // ========================================== // THE FACTORY METHOD // ========================================== // - Return type is the Product interface // - Can be abstract (must override) or virtual (optional override) // - Usually protected (only subclasses use it) // - Named descriptively: createX, makeX, buildX protected abstract createDocument(path: string): Document; // ========================================== // OPERATIONS THAT USE THE FACTORY METHOD // ========================================== // These methods call the factory method to get products // They work with the Product interface, not concrete classes processDocument(path: string): ProcessingResult { // Step 1: Create document via factory method const document = this.createDocument(path); // Step 2: Use the document through its interface document.parse(); if (!document.validate()) { return { success: false, error: 'Validation failed' }; } const rendered = document.render(); return { success: true, content: rendered }; } batchProcess(paths: string[]): ProcessingResult[] { return paths.map(path => this.processDocument(path)); } compareDocuments(path1: string, path2: string): Comparison { // Factory method called multiple times const doc1 = this.createDocument(path1); const doc2 = this.createDocument(path2); doc1.parse(); doc2.parse(); return new Comparison(doc1.render(), doc2.render()); }} // Creator with default implementationabstract class LoggingApplication { private logger: Logger | null = null; // Virtual factory method with default protected createLogger(): Logger { return new ConsoleLogger(); // Default implementation } protected getLogger(): Logger { if (!this.logger) { this.logger = this.createLogger(); } return this.logger; } log(message: string): void { this.getLogger().log(message); }}The Creator has two core aspects:
newThe Creator should NEVER reference ConcreteProduct classes. If you see imports of PdfDocument, WordDocument, etc., in the Creator, something is wrong. The Creator only knows about the Product interface. Only ConcreteCreators know about their matching ConcreteProducts.
ConcreteCreator classes override the factory method to return an instance of a ConcreteProduct. This is where the actual new call lives. Each ConcreteCreator is paired with a ConcreteProduct.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
// ConcreteCreators override the factory method // ConcreteCreator for PDF documentsclass PdfDocumentProcessor extends DocumentProcessor { protected createDocument(path: string): Document { return new PdfDocument(path); }} // ConcreteCreator for Word documentsclass WordDocumentProcessor extends DocumentProcessor { protected createDocument(path: string): Document { return new WordDocument(path); }} // ConcreteCreator for Markdown documentsclass MarkdownDocumentProcessor extends DocumentProcessor { protected createDocument(path: string): Document { return new MarkdownDocument(path); }} // ConcreteCreator with complex construction logicclass ConfiguredPdfProcessor extends DocumentProcessor { constructor( private readonly config: PdfConfig, private readonly plugins: PdfPlugin[] ) { super(); } protected createDocument(path: string): Document { // Factory method can contain complex creation logic const doc = new PdfDocument(path); // Apply configuration doc.setRenderingMode(this.config.renderingMode); doc.setDpi(this.config.dpi); // Install plugins for (const plugin of this.plugins) { doc.installPlugin(plugin); } return doc; }} // ConcreteCreator for testingclass TestDocumentProcessor extends DocumentProcessor { private createdDocuments: MockDocument[] = []; protected createDocument(path: string): Document { const mock = new MockDocument(path); this.createdDocuments.push(mock); return mock; } // Test helper methods getCreatedDocuments(): MockDocument[] { return this.createdDocuments; } getDocumentAt(index: number): MockDocument { return this.createdDocuments[index]; }}new — This is the only place in the pattern where new ConcreteProduct() appears.Notice the pairing: PdfDocumentProcessor creates PdfDocument. WordDocumentProcessor creates WordDocument. This one-to-one correspondence is typical, though not required. A ConcreteCreator could create different ConcreteProducts based on parameters or could even have a hierarchy of its own.
Understanding how these participants interact at runtime is crucial. Let's trace through a complete execution.
1234567891011121314151617181920212223
// Step 1: Client instantiates a ConcreteCreatorconst processor: DocumentProcessor = new PdfDocumentProcessor(); // Step 2: Client calls operation on Creator (polymorphically)const result = processor.processDocument('/docs/report.pdf'); // What happens inside processDocument():// // processDocument() {// // Step 3: Creator calls factory method// const document = this.createDocument(path);// // // At runtime, 'this' is PdfDocumentProcessor// // So createDocument() returns new PdfDocument(path)// // // Step 4: Creator uses Product through interface// document.parse(); // Calls PdfDocument.parse()// document.validate(); // Calls PdfDocument.validate()// document.render(); // Calls PdfDocument.render()// } // The key insight: Creator code never changes,// but different ConcreteCreators produce different behaviorThe collaboration sequence:
processDocument().this.createDocument() is called.this is ConcreteCreator, so the overridden factory method runs.new call happens in the overridden factory method.The basic Factory Method structure admits several variations depending on your needs.
Creator and Product in one hierarchy:
Sometimes the Creator and Product are the same class—the Creator creates instances of its own type.
1234567891011121314151617181920212223242526272829303132333435
// The Creator IS the Productabstract class Component { protected children: Component[] = []; // Factory method returns same type abstract clone(): Component; addChild(child: Component): void { this.children.push(child); } deepClone(): Component { const cloned = this.clone(); for (const child of this.children) { cloned.addChild(child.deepClone()); } return cloned; }} class Button extends Component { constructor(private label: string) { super(); } clone(): Component { return new Button(this.label); }} class Panel extends Component { constructor(private title: string) { super(); } clone(): Component { return new Panel(this.title); }}This is common in Prototype-like scenarios where objects need to create copies of themselves.
A distinctive characteristic of Factory Method is the creation of parallel hierarchies: one for Creators and one for Products. These hierarchies mirror each other.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// PRODUCT HIERARCHY // CREATOR HIERARCHY// --------------------- // ---------------------// // Transport (interface) // Logistics (abstract)// ├── Truck // ├── RoadLogistics// ├── Ship // ├── SeaLogistics// └── Plane // └── AirLogistics // The hierarchies are parallel:// RoadLogistics creates Truck// SeaLogistics creates Ship// AirLogistics creates Plane interface Transport { deliver(cargo: Cargo): Delivery;} abstract class Logistics { planDelivery(cargo: Cargo): DeliveryPlan { const transport = this.createTransport(); return { transport: transport, route: this.calculateRoute(cargo), estimatedTime: transport.estimateTime(cargo), }; } protected abstract createTransport(): Transport; protected abstract calculateRoute(cargo: Cargo): Route;} // Parallel implementation pairsclass Truck implements Transport { /* ... */ }class RoadLogistics extends Logistics { protected createTransport(): Transport { return new Truck(); }} class Ship implements Transport { /* ... */ }class SeaLogistics extends Logistics { protected createTransport(): Transport { return new Ship(); }} class Plane implements Transport { /* ... */ }class AirLogistics extends Logistics { protected createTransport(): Transport { return new Plane(); }}Parallel hierarchies emerge naturally from the Factory Method pattern. Each ConcreteCreator needs to create a specific ConcreteProduct, so they pair up. This parallelism makes the pattern predictable and easy to extend—add a new product, add a corresponding creator.
Trade-off awareness:
Parallel hierarchies can become a maintenance burden if they grow large. Every new product requires a new creator. Some developers consider this a downside, but the benefit is that each pair is self-contained and independently testable.
Let's consolidate our understanding of Factory Method's structure:
| Participant | Type | Primary Responsibility |
|---|---|---|
| Product | Interface/Abstract | Define interface for created objects |
| ConcreteProduct | Class | Implement Product interface with specific behavior |
| Creator | Abstract Class | Declare factory method; use it in operations |
| ConcreteCreator | Class | Override factory method to return ConcreteProduct |
new lives in ConcreteCreator — This is the only place concrete instantiation occurs.In the next page, we'll explore real-world use cases and examples of Factory Method—seeing how this structural pattern applies to document processing, UI frameworks, database connections, and more.
You now have a complete structural understanding of Factory Method. You can identify the four participants, understand their responsibilities, and see how they collaborate through the factory method to achieve polymorphic object creation.