Loading content...
Every object-oriented system faces a fundamental tension: code needs to create objects, but creating objects means knowing their concrete types. The moment you write new ConcreteProduct(), you've created a hard dependency—your code now knows about a specific implementation and cannot easily work with alternatives.
This coupling seems innocent at first. But as systems grow, it becomes a major obstacle to flexibility, testing, and evolution. What if you need different product types for different environments? What if new product types emerge? What if you want to test with mock objects?
The Factory Method pattern, powered by polymorphism, offers an elegant solution that has become one of the most widely used patterns in professional software development.
By the end of this page, you will understand how polymorphism enables flexible object creation through factory methods, why this matters for maintainability and testability, and how to apply this pattern in real-world scenarios.
To appreciate why factory methods matter, we must first understand the problems that direct instantiation creates. Consider a document processing system that needs to create different document types:
12345678910111213141516171819202122232425262728
// The naive approach: direct instantiation everywhereclass DocumentProcessor { public void processRequest(String documentType, String content) { Document doc; // Direct instantiation creates coupling if (documentType.equals("pdf")) { doc = new PdfDocument(); // Coupled to PdfDocument } else if (documentType.equals("word")) { doc = new WordDocument(); // Coupled to WordDocument } else if (documentType.equals("text")) { doc = new TextDocument(); // Coupled to TextDocument } else { throw new IllegalArgumentException("Unknown document type"); } doc.setContent(content); doc.render(); doc.save(); }} // Problems with this approach:// 1. DocumentProcessor knows about ALL document types// 2. Adding new types requires modifying DocumentProcessor// 3. The if-else chain violates Open/Closed Principle// 4. Testing requires real document implementations// 5. Different configurations (dev/prod) require code changesThis approach creates what software architects call creational coupling—the code that uses objects is tightly bound to the code that creates them. Every time a new document type is introduced, every piece of code that creates documents must be updated.
In large codebases, creational coupling can mean changing dozens or hundreds of files when a new type is introduced. This is expensive, error-prone, and discourages the addition of new types—exactly the opposite of what flexible software requires.
The Factory Method pattern addresses creational coupling by defining an interface for creating objects while allowing subclasses to decide which classes to instantiate. It's a direct application of polymorphism to the problem of object creation.
Definition: A Factory Method is a method that returns an instance of a class (usually defined by an interface or abstract class), where the actual type of the returned instance is determined by the implementing class, not the calling code.
The key insight is that polymorphism works for creation just as it works for behavior. Instead of client code deciding which object to create, it delegates that decision to a factory that can be substituted polymorphically.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
// Step 1: Define the product interfaceinterface Document { void setContent(String content); void render(); void save();} // Step 2: Define concrete productsclass PdfDocument implements Document { private String content; @Override public void setContent(String content) { this.content = content; } @Override public void render() { System.out.println("Rendering PDF with content: " + content); } @Override public void save() { System.out.println("Saving as .pdf file"); }} class WordDocument implements Document { private String content; @Override public void setContent(String content) { this.content = content; } @Override public void render() { System.out.println("Rendering Word document: " + content); } @Override public void save() { System.out.println("Saving as .docx file"); }} // Step 3: Define the factory interface with THE FACTORY METHODinterface DocumentFactory { Document createDocument(); // This is the FACTORY METHOD} // Step 4: Define concrete factoriesclass PdfDocumentFactory implements DocumentFactory { @Override public Document createDocument() { return new PdfDocument(); }} class WordDocumentFactory implements DocumentFactory { @Override public Document createDocument() { return new WordDocument(); }} // Step 5: Client code works with abstractions onlyclass DocumentProcessor { private final DocumentFactory factory; // Factory injected—no knowledge of concrete types! public DocumentProcessor(DocumentFactory factory) { this.factory = factory; } public void processRequest(String content) { // Polymorphic creation—type determined by factory Document doc = factory.createDocument(); doc.setContent(content); doc.render(); doc.save(); }}Now the DocumentProcessor has zero knowledge of concrete document types. It works entirely through the Document interface and the DocumentFactory interface. The actual type of document created is determined by whichever factory implementation is injected.
The factory method pattern works because of runtime polymorphism. When factory.createDocument() is called, the JVM looks at the actual type of factory (e.g., PdfDocumentFactory) and calls its overridden createDocument() method. The client code doesn't know or care which implementation runs.
Understanding exactly how polymorphism enables the factory method pattern deepens our appreciation of both concepts. Let's trace through the mechanics:
1234567891011121314151617181920212223
// At runtime, polymorphism operates at two levels: // LEVEL 1: Factory PolymorphismDocumentFactory factory = new PdfDocumentFactory();// ^ Reference type: DocumentFactory// ^ Actual type: PdfDocumentFactory Document doc = factory.createDocument();// The JVM performs dynamic dispatch:// 1. Looks at actual object type: PdfDocumentFactory// 2. Finds createDocument() in PdfDocumentFactory// 3. Executes PdfDocumentFactory.createDocument()// 4. Returns a PdfDocument (as Document reference) // LEVEL 2: Product Polymorphismdoc.render();// The JVM performs dynamic dispatch again:// 1. Looks at actual object type: PdfDocument// 2. Finds render() in PdfDocument// 3. Executes PdfDocument.render() // Both levels use the SAME polymorphic mechanism// The factory method pattern is polymorphism applied to creation| Level | Reference Type | Actual Type | Polymorphic Call |
|---|---|---|---|
| Factory Level | DocumentFactory | PdfDocumentFactory | createDocument() |
| Product Level | Document | PdfDocument | render(), save(), etc. |
This double polymorphism is what makes the pattern so powerful. Both the creation logic and the object behavior are determined at runtime based on actual types, not reference types. Client code operates entirely at the abstraction level.
DocumentFactory can be used; any product implementing Document can be returned.The real payoff of the factory method pattern is seen when requirements change. Suppose we need to add support for Markdown documents. With direct instantiation, we'd need to modify DocumentProcessor and every other class that creates documents. With factory methods, we simply add new classes:
1234567891011121314151617181920212223242526272829303132333435363738
// Adding Markdown support requires NO changes to existing code // Step 1: Create the new productclass MarkdownDocument implements Document { private String content; @Override public void setContent(String content) { this.content = content; } @Override public void render() { System.out.println("Rendering Markdown: " + content); } @Override public void save() { System.out.println("Saving as .md file"); }} // Step 2: Create the new factoryclass MarkdownDocumentFactory implements DocumentFactory { @Override public Document createDocument() { return new MarkdownDocument(); }} // Step 3: Use it—no changes to DocumentProcessor!DocumentFactory markdownFactory = new MarkdownDocumentFactory();DocumentProcessor processor = new DocumentProcessor(markdownFactory);processor.processRequest("# Hello Markdown"); // Existing code is completely untouched// PdfDocumentFactory, WordDocumentFactory, DocumentProcessor// all remain as they wereThe system is now OPEN for extension (adding MarkdownDocument and MarkdownDocumentFactory) but CLOSED for modification (DocumentProcessor didn't change). This is exactly what the Open/Closed Principle prescribes, and polymorphism makes it possible.
One of the most valuable applications of factory method polymorphism is enabling unit testing of code that creates objects. When creation is polymorphic, tests can inject factories that produce mock or stub objects:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
// Production code remains unchangedclass DocumentProcessor { private final DocumentFactory factory; public DocumentProcessor(DocumentFactory factory) { this.factory = factory; } public void processRequest(String content) { Document doc = factory.createDocument(); doc.setContent(content); doc.render(); doc.save(); }} // Test code: create a mock factoryclass MockDocumentFactory implements DocumentFactory { public boolean createDocumentCalled = false; public MockDocument lastCreatedDocument; @Override public Document createDocument() { createDocumentCalled = true; lastCreatedDocument = new MockDocument(); return lastCreatedDocument; }} class MockDocument implements Document { public String contentSet; public boolean renderCalled = false; public boolean saveCalled = false; @Override public void setContent(String content) { this.contentSet = content; } @Override public void render() { this.renderCalled = true; } @Override public void save() { this.saveCalled = true; }} // Unit testclass DocumentProcessorTest { @Test public void testProcessRequestCallsAllMethods() { // Arrange MockDocumentFactory mockFactory = new MockDocumentFactory(); DocumentProcessor processor = new DocumentProcessor(mockFactory); // Act processor.processRequest("Test Content"); // Assert assertTrue(mockFactory.createDocumentCalled); assertEquals("Test Content", mockFactory.lastCreatedDocument.contentSet); assertTrue(mockFactory.lastCreatedDocument.renderCalled); assertTrue(mockFactory.lastCreatedDocument.saveCalled); }}Without factory methods, testing DocumentProcessor would require instantiating real PDF or Word documents—potentially involving file I/O, external libraries, or complex setup. Factory method polymorphism decouples testing from implementation.
Factory methods are often combined with Dependency Injection (DI) frameworks. The DI container is configured to provide specific factory implementations (production vs. test), and the application code never knows which concrete factories it receives.
Factory methods can accept parameters to influence creation decisions while still maintaining polymorphic flexibility. This pattern is common when the factory needs context to determine what to create:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
// Parameterized factory methodinterface NotificationFactory { Notification createNotification(NotificationPriority priority);} enum NotificationPriority { LOW, NORMAL, HIGH, URGENT} interface Notification { void send(String message, String recipient);} // Factory that creates different notification types based on priorityclass SmartNotificationFactory implements NotificationFactory { @Override public Notification createNotification(NotificationPriority priority) { switch (priority) { case LOW: return new EmailNotification(); // Low priority: email only case NORMAL: return new PushNotification(); // Normal: push notification case HIGH: return new CompositeNotification( new PushNotification(), new SmsNotification() ); // High: push + SMS case URGENT: return new CompositeNotification( new PushNotification(), new SmsNotification(), new PhoneCallNotification() ); // Urgent: push + SMS + phone call default: return new EmailNotification(); } }} // Different factory implementation for test environmentclass TestNotificationFactory implements NotificationFactory { @Override public Notification createNotification(NotificationPriority priority) { // Always return mock notifications in test return new MockNotification(); }} // Client code uses factory polymorphicallyclass AlertService { private final NotificationFactory factory; public AlertService(NotificationFactory factory) { this.factory = factory; } public void sendAlert(String message, String recipient, NotificationPriority priority) { Notification notification = factory.createNotification(priority); notification.send(message, recipient); }}In this design, the factory method accepts a parameter but the client code is still decoupled from the specific notification types. Different factory implementations (production vs. test) can interpret the parameters differently while maintaining the same interface.
A related pattern is the static factory method—a static method that returns an instance of its class (or a subtype). While not polymorphic on the factory itself, these methods often return polymorphic types and offer several advantages over constructors:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
// Static factory methods in Javapublic class Currency { private final String code; private final int minorUnits; // Private constructor—clients can't call directly private Currency(String code, int minorUnits) { this.code = code; this.minorUnits = minorUnits; } // Static factory method with descriptive name public static Currency ofCode(String code) { // Can implement caching, validation, or return subtypes return CURRENCY_CACHE.computeIfAbsent(code, c -> new Currency(c, getMinorUnitsForCode(c))); } // Factory method can return different subtypes public static Currency usd() { return ofCode("USD"); } public static Currency eur() { return ofCode("EUR"); }} // Another example: returning polymorphic typespublic abstract class Shape { public abstract double area(); // Static factory returning appropriate subtype public static Shape circle(double radius) { return new Circle(radius); // Returns a Circle } public static Shape rectangle(double width, double height) { return new Rectangle(width, height); // Returns a Rectangle } public static Shape square(double side) { return new Rectangle(side, side); // Can reuse implementations }} // Usage is clean and readableShape circle = Shape.circle(5.0);Shape rect = Shape.rectangle(4.0, 3.0);double totalArea = circle.area() + rect.area();ofCode("USD") is clearer than new Currency("USD", 2).Static factory methods are convenient but not substitutable via polymorphism. Use instance-based factory methods (the Factory Method pattern) when you need to swap factory implementations at runtime or for testing.
Factory methods are pervasive in professional software development. Here are examples from widely-used frameworks and libraries:
| Framework/Library | Factory Method | Returns |
|---|---|---|
| Java Collections | List.of(), Map.of() | Immutable collection implementations |
| Java Optional | Optional.of(), Optional.empty() | Optional wrapper instances |
| Spring Framework | BeanFactory.getBean() | Bean instances based on configuration |
| JDBC | DriverManager.getConnection() | Database connection implementations |
| Java Logging | LoggerFactory.getLogger() | Logger implementations (SLF4J) |
| Java Executors | Executors.newFixedThreadPool() | ExecutorService implementations |
123456789101112131415161718192021222324
// Java Collections: Factory methods return optimized implementationsList<String> emptyList = List.of(); // Returns EmptyListList<String> singletonList = List.of("one"); // Returns SingletonListList<String> smallList = List.of("a", "b"); // Returns List12List<String> largerList = List.of("a", "b", "c", "d", "e"); // Returns ListN // SLF4J: Factory returns logger bound at runtimeLogger logger = LoggerFactory.getLogger(MyClass.class);// At runtime, this might return:// - Logback's ch.qos.logback.classic.Logger// - Log4j's org.apache.logging.log4j.spi.AbstractLogger// - Java util logging adapter// - A no-operation logger // JDBC: Factory returns database-specific connectionConnection conn = DriverManager.getConnection( "jdbc:postgresql://localhost/db", "user", "pass");// Returns PostgreSQL's PgConnection, but client sees only Connection interface // Executors: Factory creates thread pool implementationsExecutorService pool = Executors.newCachedThreadPool();// Returns ThreadPoolExecutor configured for caching, but client sees only// the ExecutorService interfaceNotice how in all these examples, the caller receives an interface type (List, Logger, Connection, ExecutorService) while the factory decides which concrete implementation to return. This is polymorphism enabling implementation hiding at the creation level.
Factory methods demonstrate that polymorphism isn't just about calling methods on objects—it's about enabling flexibility at every level of object-oriented design, including object creation itself.
new ConcreteClass() binds code to specific implementations.What's next:
In the next page, we'll explore the Strategy pattern—another powerful application of polymorphism that enables algorithms to be selected and swapped at runtime. Where factory methods make object creation polymorphic, strategy makes behavior polymorphic.
You now understand how polymorphism powers the Factory Method pattern, enabling flexible, extensible, and testable object creation. This pattern is fundamental to professional software development and appears throughout modern frameworks and libraries.