Factory Method Design Pattern

Factory for objects

Factory design pattern belongs to category of creational design pattern. It is one of the very important and frequently used design patterns.

Lets start with the questions. What is it? When can I use?

Before moving forward, let me give you a few scenarios for better clarity.
Take some real-life examples:
Bank Account: There are different types of bank accounts, such as Savings, Current, and Joint Accounts, etc.
Food Order: Here, you have various order types, like Delivery, Dine-In, and Takeaway.
Vehicles: We have categories like two-wheelers, three-wheelers, and four-wheelers.
Messages: There are different types of messages, such as SMS, WhatsApp, Telegram, etc. For all these types they have their own implementation and flow. Let's see the below example.

Consider a payment system with different types of payment modes.

public class PaymentProcessor {

    public void processPayment(String paymentType, double amount) {
        if (paymentType.equalsIgnoreCase("UPI")) {
            processUPIPayment(amount);
        } else if (paymentType.equalsIgnoreCase("CreditCard")) {
            processCreditCardPayment(amount);
        } else if (paymentType.equalsIgnoreCase("DebitCard")) {
            processDebitCardPayment(amount);
        } else {
            System.out.println("Invalid payment type!");
        }
    }

    private void processUPIPayment(double amount) {
        System.out.println("Processing UPI payment of amount: " + amount);
    }

    private void processCreditCardPayment(double amount) {
        System.out.println("Processing Credit Card payment of amount: " + amount);
    }

    private void processDebitCardPayment(double amount) {
        System.out.println("Processing Debit Card payment of amount: " + amount);
    }
    //Here main is our client.
    public static void main(String[] args) {
        PaymentProcessor paymentProcessor = new PaymentProcessor();
        paymentProcessor.processPayment("UPI", 500.0);
        paymentProcessor.processPayment("CreditCard", 1000.0);
        paymentProcessor.processPayment("DebitCard", 750.0);
        paymentProcessor.processPayment("Cash", 300.0); // Invalid type
    }
}

To implement these scenarios, we use normal if-else statements. This approach violates the SOLID principles:

Single Responsibility Principle (SRP): A class should have only one reason to change. But in this case, we are modifying the same class whenever a new feature (like a new account type or order type) is added, which goes against SRP.

Open/Closed Principle (OCP): The Open/Closed Principle states that classes should be open for extension but closed for modification, but by modifying the existing class to add new types or features, we are violating this rule.

Dependency Inversion Principle (DIP): This principle says that high-level modules should not depend on low-level modules, both should depend on abstractions. If we use if-else logic DIP gets violated because the class where you implement this logic depends directly on concrete implementations instead of abstractions/Interface

Liskov Substitution Principle (LSP): By using if-else there is no subclasses and base class concept here, hinting at deeper design issues.

If we dont follow these principles. we cannot achieve a cleaner, more extensible code. we can solve this issue by using factory method design pattern. This pattern allows us to extend functionality without modifying existing code

The Factory Method is a design pattern that defines an interface for creating objects but allows subclasses/concrete classes to decide which class to instantiate.

This decision happens at runtime. This is an example of polymorphism, which is why we say that design patterns are built on top of OOP concepts. The data type of the reference object depends on the subclass or concrete class.

It helps to achieve loose coupling between the client code and the object creation process. This hides the object creation logic from the client.

Why is this useful?
It’s helpful when your program needs to work with many different types of objects. For example payment types (like UPI, CREDIT CARD, or DEBIT CARD) but doesn’t know in advance which specific type is needed because the client decides which payment mode. By using the Factory Method, you can abstract the logic of object creation for same object types. By this way you can make the program flexible and open to new types of objects without changing the main logic.

Structure/UML Diagram

ILogger Interface: Contains the log(String) method, which must be implemented by all different logger types.
Concrete Loggers (InfoLoggerImpl, DebugLoggerImpl, ErrorLoggerImpl): Each Class implements the log(String) method and logs the message with a prefix (INFO, DEBUG, ERROR).
LoggerFactory: Contains the method loggerFactory(String type), which returns the appropriate logger object based on the type parameter (INFO, DEBUG, ERROR). Main/Client Class: Uses the LoggerFactory to get the correct logger and logs messages accordingly.

public class LoggerImpl {
    public interface Ilogger{
       void log(String logMessage);
    }
    //Now Implementation of Info Logger
    public static class InfoLogger implements Ilogger{
        @Override
        public void log(String logMessage) {
            System.out.println("Info: " + logMessage);
        }
    }

    //Now Implementation of Debug Logger
    public static class DebugLogger implements Ilogger{
        @Override
        public void log(String logMessage) {
            System.out.println("Debug: " + logMessage);
        }
    }

    //Now Implementation of Error Logger
    public static class ErrorLogger implements Ilogger{
        @Override
        public void log(String logMessage) {
            System.out.println("Error: " + logMessage);
        }
    }

    public static class LoggerFactory{
        public static Ilogger getLogger(String type){
            if(type.equalsIgnoreCase("INFO")){
                return new InfoLogger();
            }else if(type.equalsIgnoreCase("DEBUG")){
                return new DebugLogger();
            } else if(type.equalsIgnoreCase("ERROR")){
                return new ErrorLogger();
            } else {
                throw new IllegalArgumentException("Invalid Logger Type");
            }
        }
    }



    public static void main(String[] args) {
        Ilogger info= LoggerFactory.getLogger("INFO");
        Ilogger debug = LoggerFactory.getLogger("DEBUG");
        Ilogger error = LoggerFactory.getLogger("ERROR");
       info.log("call to info logger");
       debug.log("call to debug logger");
       error.log("call to error logger");

    }


}

This implementation follows the Factory Method Design Pattern and this logging system can be easily extended to include additional loggers without modifying the existing code (following the Open/Closed Principle).

If you observe closely, we are violating the Open/Closed Principle (OCP) in the LoggerFactory class. When we need to add a new logger, we have to modify the existing code by adding a new else-if block. This violates the OCP. So this implementation is not Factory Method Design Pattern. It is compact version called simple Factory Design Pattern.

To follow to the OCP principle, we need to introduce another layer of abstraction. This would allow us to extend the system by adding new loggers without modifying the existing factory class.

Structure/UML diagram

public interface Ilogger {
    void log(String message);
}
//Implementation classes
public class InfoLoggerImpl implements Ilogger{
    @Override
    public void log(String message) {
        System.out.println("[INFO] " + message );
    }
}

public class DebugLogger implements Ilogger{
    @Override
    public void log(String message) {
        System.out.println("[DEBUG] " + message );
    }
}

public class ErrorLogger implements Ilogger{
    @Override
    public void log(String message) {
        System.out.println("[ERROR] " + message );
    }
}
//Abstact class
public abstract class LoggerFactory {
    public abstract Ilogger createLogger();
}
//Implementation classes
public class InfoLoggerFactory extends LoggerFactory{
    @Override
    public Ilogger createLogger() {
        return new InfoLoggerImpl();
    }
}
public class DebugLoggerFactory extends LoggerFactory{
    @Override
    public Ilogger createLogger() {
        return new DebugLogger();
    }
}
public class ErrorLoggerFactory extends LoggerFactory{
    @Override
    public Ilogger createLogger() {
        return new ErrorLogger();
    }
}

    //first create a factory object.
    //then use the factory object to create Ilogger object
    public static void main(String[] args) {
        LoggerFactory infoLogger = new InfoLoggerFactory();
        Ilogger infoLoggerLogger = infoLogger.createLogger();
        infoLoggerLogger.log("call to info");

        LoggerFactory debugLogger = new DebugLoggerFactory();
        Ilogger debugLoggerLogger = debugLogger.createLogger();
        debugLoggerLogger.log("call to debug");

        LoggerFactory errorLogger = new ErrorLoggerFactory();
        Ilogger errorLog = errorLogger.createLogger();
        errorLog.log("call to error");
    }

Mostly we follow simple factory design pattern to avoid more classes, because simple factory provides a way to centralize object creation logic within a single factory class. It reduces the overhead of creating multiple subclasses or factories, which is often unnecessary when the requirements are relatively stable, and the system does not require frequent extensions.

Stay tuned for more on design principles in my next blogs. 😊