Mediumcreational20 min read

Factory and Factory Method Pattern

1. What is Factory Pattern?

Definition

The Factory Pattern is a GOF Creational Pattern that provides a way to create objects without exposing the creation logic to the client. The client refers to newly created objects using a common interface or base class, not the concrete class.

One line: A factory class internally contains a factory method that creates and returns one of several related class objects based on the data we pass.

Sometimes we have a class, but do not know how to create the object for that class because objects can be created in different ways: using new operator, using factory method, using reflection API, etc. In such situations, instead of the client creating the object itself, we delegate the creation to a Factory class that hides the details.

Real-world examples

  • CarFactory: Gives a car based on the model type you pass, without the customer knowing how the car is manufactured.
  • Bakery: Gives a cake based on the type you request, without the customer knowing how the cake is prepared.
  • DriverManager.getConnection(...): Gives an Oracle JDBC connection object if the URL has Oracle details, or a MySQL JDBC connection object if the URL has MySQL details. The client does not worry about how the connection is created.
  • Spring Framework: factory.getBean(...) is a factory pattern. Spring provides abstraction on bean class object creation and returns the object to the client when needed.

Factory Pattern has two parts

  1. Factory Pattern internally contains one static factory method or non-static method and provides abstraction on the object creation process.
  2. Factory Pattern creates and returns one of several related class objects based on the data we have passed.

Pre-defined Factory Methods in JDK

Factory MethodStatic / Non-StaticReturns
Thread.currentThread()StaticReturns its own class object (Thread)
Class.forName("java.util.Date")StaticReturns its own class object (Class)
Calendar.getInstance()StaticReturns related class object (GregorianCalendar subclass)
Properties.getProperties()StaticReturns related class object (between System class and Properties class)
DriverManager.getConnection(url, user, pwd)StaticReturns related class object (driver-specific Connection)
con.createStatement()Non-StaticReturns related class object (called on Connection object)
st.executeQuery()Non-StaticReturns related class object (ResultSet)
Note

In fact, most of the JDBC methods like con.createStatement(), st.executeQuery(), and others are factory methods. They create and return objects without exposing how the underlying driver creates them.

2. The Problem: Tight Coupling

Problem

When the client directly creates objects of concrete classes using new, it leads to:

  • Every Client/Customer must know how to create the object. If the object has dependent objects, they must also know how to create those and add them to the main object.
  • Creating multiple objects for multiple related classes and using one/few of them is bad design.
  • If a concrete class name changes, every client must be modified and recompiled.
  • Object creation logic is scattered and duplicated across all clients.

Problem Code: Car hierarchy without factory

Car.java (abstract base class)

package com.nt.comps;

public abstract class Car {
    private int engineId;
    private String regNo;

    public abstract void assemble();
    public abstract void roadTest();
    public abstract void drive();

    // getters and setters
    public int getEngineId() { return engineId; }
    public void setEngineId(int engineId) { this.engineId = engineId; }
    public String getRegNo() { return regNo; }
    public void setRegNo(String regNo) { this.regNo = regNo; }
}

SportsCar.java

package com.nt.comps;

public class SportsCar extends Car {
    private String speed;
    private String pickup;

    public SportsCar() {
        System.out.println("SportsCar:: 0-param constructor");
    }

    @Override
    public void assemble() {
        System.out.println("SportsCar::assemble()");
    }

    @Override
    public void roadTest() {
        System.out.println("SportsCar::roadTest()");
    }

    @Override
    public void drive() {
        System.out.println("SportsCar::drive()");
    }
}

LuxuryCar.java

package com.nt.comps;

public class LuxuryCar extends Car {
    private String bootspace;
    private String security;

    public LuxuryCar() {
        System.out.println("LuxuryCar:: 0-param constructor");
    }

    @Override
    public void assemble() {
        System.out.println("LuxuryCar::assemble()");
    }

    @Override
    public void roadTest() {
        System.out.println("LuxuryCar::roadTest()");
    }

    @Override
    public void drive() {
        System.out.println("LuxuryCar::drive()");
    }
}

BudgetCar.java

package com.nt.comps;

public class BudgetCar extends Car {
    private String fuelEconomy;
    private float price;

    public BudgetCar() {
        System.out.println("BudgetCar:: 0-param constructor");
    }

    @Override
    public void assemble() {
        System.out.println("BudgetCar::assemble()");
    }

    @Override
    public void roadTest() {
        System.out.println("BudgetCar::roadTest()");
    }

    @Override
    public void drive() {
        System.out.println("BudgetCar::drive()");
    }
}

Problem client classes (tightly coupled)

// CarCustomer1.java - client must import and know SportsCar
package com.nt.test;

import com.nt.comps.Car;
import com.nt.comps.SportsCar;

public class CarCustomer1 {
    public static void main(String[] args) {
        Car sCar = new SportsCar();  // tightly coupled to SportsCar
        sCar.assemble();
        sCar.roadTest();
        sCar.drive();
    }
}

// CarCustomer2.java - client must import and know BudgetCar
package com.nt.test;

import com.nt.comps.BudgetCar;
import com.nt.comps.Car;

public class CarCustomer2 {
    public static void main(String[] args) {
        Car bCar = new BudgetCar();  // tightly coupled to BudgetCar
        bCar.assemble();
        bCar.roadTest();
        bCar.drive();
    }
}

// CarCustomer3.java - client must import and know LuxuryCar
package com.nt.test;

import com.nt.comps.Car;
import com.nt.comps.LuxuryCar;

public class CarCustomer3 {
    public static void main(String[] args) {
        Car lCar = new LuxuryCar();  // tightly coupled to LuxuryCar
        lCar.assemble();
        lCar.roadTest();
        lCar.drive();
    }
}
Why is this bad?

Every Client/Customer must know to create Car object. If Car object is having any dependent objects, they should also know how to create those dependent objects and how to add those objects to the main object. Creating multiple objects for multiple related classes and using one/few of them is bad thought.

To overcome these problems we need to add a Factory Pattern class which contains one factory method taking care of object creation process by providing abstraction from customers/clients.

3. The Solution: Simple Factory

Solution

Move the object creation logic into a separate Factory class. The client only knows the base class (Car) and the factory. It passes a car type String; the factory decides which concrete class to instantiate. The client never uses new on concrete classes and never imports them.

CarFactory.java

package com.nt.factory;

import com.nt.comps.Car;
import com.nt.comps.SportsCar;
import com.nt.comps.LuxuryCar;
import com.nt.comps.BudgetCar;

public class CarFactory {

    // STATIC factory method
    public static Car createCar(String carType) {
        if (carType == null) {
            throw new IllegalArgumentException("car type cannot be null");
        }

        Car car;
        if (carType.equalsIgnoreCase("sports")) {
            car = new SportsCar();
        } else if (carType.equalsIgnoreCase("luxury")) {
            car = new LuxuryCar();
        } else if (carType.equalsIgnoreCase("budget")) {
            car = new BudgetCar();
        } else {
            throw new IllegalArgumentException("Invalid car type: " + carType);
        }
        return car;
    }
}

Solution client classes (loosely coupled)

// CarCustomer1.java - client knows only Car + CarFactory
package com.nt.test;

import com.nt.comps.Car;
import com.nt.factory.CarFactory;

public class CarCustomer1 {
    public static void main(String[] args) {
        // client is loosely coupled - no concrete class imported
        Car car = CarFactory.createCar("sports");
        car.assemble();
        car.roadTest();
        car.drive();
    }
}

// CarCustomer2.java - same factory, different type
package com.nt.test;

import com.nt.comps.Car;
import com.nt.factory.CarFactory;

public class CarCustomer2 {
    public static void main(String[] args) {
        Car car = CarFactory.createCar("luxury");
        car.assemble();
        car.roadTest();
        car.drive();
    }
}

// Adding a new car type now requires a change ONLY in CarFactory,
// not in any client class.
SportsCar:: 0-param constructor SportsCar::assemble() SportsCar::roadTest() SportsCar::drive()
Key Benefit

Note that the client imports com.nt.comps.Car (the base class) and com.nt.factory.CarFactory (the factory). It does NOT import SportsCar, LuxuryCar, or BudgetCar. If a new car type is added or a class is renamed, only CarFactory changes. The factory is now developed by another developer. That "other one" we pass the description of the object to in the factory method.

4. What is a Factory Method?

Definition

The method that is capable of creating and returning either its own class object, or a related class object, or an unrelated class object (in fact capable of returning any object) is called a factory method.

Two types of factory methods

1. Static factory method

Invokable on the class name directly, without creating an object first.

// Returns its own class object
Thread t = Thread.currentThread();
Class c = Class.forName("java.util.Date");

// Returns related class object (subclass)
Calendar cal = Calendar.getInstance();
// returns GregorianCalendar which is a subclass of Calendar

// Returns related class object (JDBC)
Connection con = DriverManager.getConnection("oracle_url", "user", "pwd");
// oracle con obj

Connection con = DriverManager.getConnection("mysql_url", "user", "pwd");
// mysql con obj

2. Non-static factory method

Invokable on a Java class object. Requires creating an instance first.

// Returns same class object
String s = new String("a1");
String s2 = s.concat("hello");  // returns same class obj

// Returns unrelated class object
StringBuffer sb = new StringBuffer("hello how are u");
String s3 = sb.substring(0, 5);  // returns String (unrelated)

// Returns related class object
CarFactory factory = new CarFactory();
Car car1 = factory.getCar("budget");  // gives related class obj
// BudgetCar extends Car

5. Factory Method vs Factory Pattern

Key Distinction

Factory Method creates and returns an object. That object can be the same class, a related class, or an unrelated class.

Factory Pattern contains a factory method but it always returns one of several related class objects based on the input. The pattern is about the class structure, not just the method.

AspectFactory MethodFactory Pattern
ScopeA single method that creates and returns an objectA class structure that uses a factory method to create related objects
Return typeCan return any Java class objectAlways returns one of several related class objects
ExamplesProperties.getProperty() is a factory method but not a factory patternDriverManager.getConnection() is a factory method AND uses factory pattern

Conclusion: Factory method can return any Java class object. Factory pattern returns one of several related classes. Every factory pattern uses a factory method, but not every factory method is a factory pattern.

6. Three Types of Factory Design Patterns

All three patterns use a factory method. The difference is in structure and flexibility:

PatternDescriptionStructureWhen to Use
Simple FactoryOne factory class, one method, creates objects via if/else or switch1 factory class, many productsSimple object creation, few product types
Factory MethodAbstract factory class defines the creation method. Subclasses decide which class to instantiate1 abstract factory, many concrete factory subclassesMultiple factories for same product family, each with its own logic
Abstract FactoryFactory of factories. Provides interface for creating families of related objectsFactory that returns other factoriesMultiple product families (e.g., JDBC vs Hibernate)
Important

"Factory method" in Java terminology means a Java method which creates and returns an object. "Factory method pattern" is a design pattern which internally uses a factory pattern with factory method. Do not confuse the two.

Relationship: Singleton class allows to create only one object. Factory Pattern provides abstraction on object creation.

7. Factory Method Pattern: Bajaj Bike Example

Scenario

Bajaj has multiple manufacturing factories (Chennai for South customers, Noida for North customers). Both factories produce the same family of bikes (Pulsor, Discover) but each factory follows its own manufacturing standards. We need a common ordering process while letting each factory decide how to create the bike.

Analogy: Factory Method pattern is like RBI for banks, TRAI for Telecom companies, IRDA for insurance companies. It is the super class for the factories who are creating objects of same family classes, providing standards to them toward object creation.

Class diagram

BajajFactory (abstract) - abstract painting() - abstract assembling() - abstract engineTest() - abstract roadTest() - abstract createBike(String type) [Factory Method] + public BajajBike orderBike(String type) [Template Method] | +-----------+-----------+ | | ChennaiBajajFactory NoidaBajajFactory (for South customers) (for North customers) - createBike() - createBike() - painting() - painting() - assembling() - assembling() Products (com.nt.bike): BajajBike (abstract base) | +--------+---------+ | | PulsorBike DiscoverBike

BajajBike.java (base product)

package com.nt.bike;

public abstract class BajajBike {
    public abstract void bikeInfo();
}

PulsorBike.java

package com.nt.bike;

public class PulsorBike extends BajajBike {
    @Override
    public void bikeInfo() {
        System.out.println("PulsorBike :: sporty bike");
    }
}

DiscoverBike.java

package com.nt.bike;

public class DiscoverBike extends BajajBike {
    @Override
    public void bikeInfo() {
        System.out.println("DiscoverBike :: commuter bike");
    }
}

BajajFactory.java (abstract factory with factory method + template method)

package com.nt.factory;

import com.nt.bike.BajajBike;

public abstract class BajajFactory {

    // Method declarations - subclasses implement these
    protected abstract void painting();
    protected abstract void assembling();
    protected abstract void engineTest();
    protected abstract void roadTest();

    // FACTORY METHOD - subclasses decide which concrete bike to create
    public abstract BajajBike createBike(String bikeType);

    // TEMPLATE METHOD - fixed order, uses factory method
    public final BajajBike orderBike(String bikeType) {
        BajajBike bike = createBike(bikeType);  // created by subclass
        painting();
        assembling();
        engineTest();
        roadTest();
        return bike;
    }
}

ChennaiBajajFactory.java (for South customers)

package com.nt.factory;

import com.nt.bike.BajajBike;
import com.nt.bike.PulsorBike;
import com.nt.bike.DiscoverBike;

public class ChennaiBajajFactory extends BajajFactory {

    @Override
    public BajajBike createBike(String bikeType) {
        System.out.println("Chennai plant creating bike...");
        if (bikeType.equalsIgnoreCase("pulsor")) return new PulsorBike();
        if (bikeType.equalsIgnoreCase("discover")) return new DiscoverBike();
        throw new IllegalArgumentException("Invalid bike type: " + bikeType);
    }

    @Override protected void painting() { System.out.println("Chennai: painting"); }
    @Override protected void assembling() { System.out.println("Chennai: assembling"); }
    @Override protected void engineTest() { System.out.println("Chennai: engine test"); }
    @Override protected void roadTest() { System.out.println("Chennai: road test"); }
}

NoidaBajajFactory.java (for North customers)

package com.nt.factory;

import com.nt.bike.BajajBike;
import com.nt.bike.PulsorBike;
import com.nt.bike.DiscoverBike;

public class NoidaBajajFactory extends BajajFactory {

    @Override
    public BajajBike createBike(String bikeType) {
        System.out.println("Noida plant creating bike...");
        if (bikeType.equalsIgnoreCase("pulsor")) return new PulsorBike();
        if (bikeType.equalsIgnoreCase("discover")) return new DiscoverBike();
        throw new IllegalArgumentException("Invalid bike type: " + bikeType);
    }

    @Override protected void painting() { System.out.println("Noida: painting"); }
    @Override protected void assembling() { System.out.println("Noida: assembling"); }
    @Override protected void engineTest() { System.out.println("Noida: engine test"); }
    @Override protected void roadTest() { System.out.println("Noida: road test"); }
}

Client classes

// SouthCustomer.java
package com.nt.test;

import com.nt.bike.BajajBike;
import com.nt.factory.BajajFactory;
import com.nt.factory.ChennaiBajajFactory;

public class SouthCustomer {
    public static void main(String[] args) {
        BajajFactory factory = new ChennaiBajajFactory();
        BajajBike bike = factory.orderBike("pulsor");
        bike.bikeInfo();
    }
}

// NorthCustomer.java
package com.nt.test;

import com.nt.bike.BajajBike;
import com.nt.factory.BajajFactory;
import com.nt.factory.NoidaBajajFactory;

public class NorthCustomer {
    public static void main(String[] args) {
        BajajFactory factory = new NoidaBajajFactory();
        BajajBike bike = factory.orderBike("discover");
        bike.bikeInfo();
    }
}
// SouthCustomer output: Chennai plant creating bike... Chennai: painting Chennai: assembling Chennai: engine test Chennai: road test PulsorBike :: sporty bike // NorthCustomer output: Noida plant creating bike... Noida: painting Noida: assembling Noida: engine test Noida: road test DiscoverBike :: commuter bike
Key Point

Here both factories are creating same Bajaj bike family class objects. Since both factories are independent factories, they are following their own standards while manufacturing bikes (creating objects). Due to this, quality differences are coming in the same family (same company) bikes.

The abstract BajajFactory fixes the manufacturing algorithm (orderBike = create + paint + assemble + engineTest + roadTest). Only the createBike() factory method varies per plant. The client picks a factory and calls orderBike() without knowing which concrete bike class is produced.

8. Abstract Factory: Factory of Factories

Definition

Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is a factory of factories.

DAO Example: Switching between JDBC and Hibernate

ApplicationFactory (Factory of Factories) | DAOFactory (Abstract) / \ JdbcDAOFactory HibernateDAOFactory | | EmployeeJdbc EmployeeHibernate DAOImpl DAOImpl
// DAO Interface
public interface IEmployeeDAO {
    void insert(EmployeeBO bo);
    EmployeeBO find(int empno);
}

// JDBC Implementation
public class EmployeeJdbcDAOImpl implements IEmployeeDAO {
    public void insert(EmployeeBO bo) { /* JDBC logic */ }
    public EmployeeBO find(int empno) { /* JDBC logic */ return null; }
}

// Hibernate Implementation
public class EmployeeHibernateDAOImpl implements IEmployeeDAO {
    public void insert(EmployeeBO bo) { /* Hibernate logic */ }
    public EmployeeBO find(int empno) { /* Hibernate logic */ return null; }
}

// Abstract Factory
public abstract class DAOFactory {
    public abstract IEmployeeDAO getEmployeeDAO();

    public static DAOFactory getInstance(String type) {
        if ("jdbc".equals(type)) return new JdbcDAOFactory();
        if ("hibernate".equals(type)) return new HibernateDAOFactory();
        throw new IllegalArgumentException("Unknown DAO type: " + type);
    }
}

// Concrete Factories
public class JdbcDAOFactory extends DAOFactory {
    public IEmployeeDAO getEmployeeDAO() {
        return new EmployeeJdbcDAOImpl();
    }
}

public class HibernateDAOFactory extends DAOFactory {
    public IEmployeeDAO getEmployeeDAO() {
        return new EmployeeHibernateDAOImpl();
    }
}

// ApplicationFactory (factory of factories)
public class ApplicationFactory {
    public static DAOFactory getDAOFactory(String type) {
        return DAOFactory.getInstance(type);
    }
}

All Three Patterns Compared

AspectSimple FactoryFactory MethodAbstract Factory
StructureOne factory class creates all objectsAbstract factory, concrete factory subclasses decide which class to instantiateFactory of factories, creates families of related objects
Approachif/else or switch in one methodInheritance-based (subclass overrides factory method)Composition-based (factory returns other factories)
ProductsOne product type, multiple variantsOne product type, factory-specific creationMultiple product types, family-based
FlexibilityLow (adding types requires changing factory)Medium (new factory subclass for new logic)High (new family = new factory, no changes to existing)
Use whenFew product types, simple creationEach factory needs different creation logicMultiple product families (JDBC vs Hibernate, UI themes)

9. Full-Length Industry Project: Payment Gateway System

Business Scenario

National Bank of India is building an online payment system. Customers can pay using UPI, Credit Card, or Net Banking. Each payment method has different validation rules, processing logic, and confirmation flows. The bank wants to add new payment methods (Debit Card, Wallet) in the future without changing existing code. The system must be loosely coupled so that adding a new payment method requires only adding new classes, not modifying existing ones.

High-Level Design (HLD)

+------------------+ | Client Tier | | Browser/Mobile | +--------+---------+ | v +------------------------------------------------------+ | CONTROLLER LAYER | | PaymentController | | - receives payment request with type and amount | | - delegates to PaymentService | +----------------------------+-------------------------+ | v +------------------------------------------------------+ | SERVICE LAYER | | PaymentService | | - uses PaymentProcessorFactory to get processor | | - validates, processes, confirms | +----------------------------+-------------------------+ | v +------------------------------------------------------+ | FACTORY LAYER (Factory Pattern) | | | | PaymentProcessorFactory | | +-- createProcessor("upi") --> UPIProcessor | | +-- createProcessor("card") --> CreditCardProc | | +-- createProcessor("netbank") --> NetBankingProc | +------------------------------------------------------+ | v +------------------------------------------------------+ | PROCESSOR LAYER (Products) | | | | PaymentProcessor (interface) | | | | | +-- UPIProcessor | | +-- CreditCardProcessor | | +-- NetBankingProcessor | | +-- (future: DebitCardProcessor, WalletProc...) | +------------------------------------------------------+ | v +------------------------------------------------------+ | EXTERNAL SYSTEMS | | UPI Gateway | Card Network | Bank Server | +------------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | PaymentProcessor (interface) | +-------------------------------------------------------+ | + validate(request): ValidationResult | | + process(request): PaymentResult | | + getProviderName(): String | +-------------------------------------------------------+ ^ ^ ^ | | | +----------------+ +------------------+ +-------------------+ | UPIProcessor | | CreditCardProc | | NetBankingProc | +----------------+ +------------------+ +-------------------+ | - upiId: String| | - cardNumber | | - bankCode | | | | - cvv | | - accountNumber | +----------------+ +------------------+ +-------------------+ | + validate() | | + validate() | | + validate() | | + process() | | + process() | | + process() | | + getProvider()| | + getProvider() | | + getProvider() | +----------------+ +------------------+ +-------------------+ +-------------------------------------------------------+ | PaymentProcessorFactory (Simple Factory) | +-------------------------------------------------------+ | + createProcessor(type): PaymentProcessor [static] | +-------------------------------------------------------+ | Reads type string, returns correct processor. | | New type = add one else-if. No client changes. | +-------------------------------------------------------+ +-------------------------------------------------------+ | PaymentService | +-------------------------------------------------------+ | - factory: PaymentProcessorFactory | | - logger: (prints to console) | +-------------------------------------------------------+ | + processPayment(request): PaymentResult | +-------------------------------------------------------+ +-------------------------------------------------------+ | Model Classes | +-------------------------------------------------------+ | PaymentRequest: type, amount, currency, metadata | | PaymentResult: success, transactionId, message | | ValidationResult: valid, errors | +-------------------------------------------------------+

Project Structure

nbi-payment-gateway/ ├── src/ │ └── main/java/com/nbi/payment/ │ ├── model/ │ │ ├── PaymentRequest.java │ │ ├── PaymentResult.java │ │ └── ValidationResult.java │ ├── processor/ │ │ ├── PaymentProcessor.java (interface) │ │ ├── UPIProcessor.java │ │ ├── CreditCardProcessor.java │ │ └── NetBankingProcessor.java │ ├── factory/ │ │ └── PaymentProcessorFactory.java (simple factory) │ ├── service/ │ │ └── PaymentService.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/payment/ │ ├── factory/ │ │ └── PaymentProcessorFactoryTest.java │ └── service/ │ └── PaymentServiceTest.java └── pom.xml

PaymentRequest.java

package com.nbi.payment.model;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

public class PaymentRequest {
    private final String type;
    private final BigDecimal amount;
    private final String currency;
    private final Map<String, String> metadata;

    public PaymentRequest(String type, BigDecimal amount) {
        this.type = type;
        this.amount = amount;
        this.currency = "INR";
        this.metadata = new HashMap<>();
    }

    public PaymentRequest withMetadata(String key, String value) {
        this.metadata.put(key, value);
        return this;
    }

    public String getType() { return type; }
    public BigDecimal getAmount() { return amount; }
    public String getCurrency() { return currency; }
    public String getMetadata(String key) { return metadata.getOrDefault(key, ""); }

    @Override
    public String toString() {
        return type.toUpperCase() + " payment of " + currency + " " + amount;
    }
}

PaymentResult.java

package com.nbi.payment.model;

import java.util.UUID;

public class PaymentResult {
    private final boolean success;
    private final String transactionId;
    private final String message;

    private PaymentResult(boolean success, String transactionId, String message) {
        this.success = success;
        this.transactionId = transactionId;
        this.message = message;
    }

    public static PaymentResult success(String message) {
        return new PaymentResult(true, generateTxnId(), message);
    }

    public static PaymentResult failure(String message) {
        return new PaymentResult(false, null, message);
    }

    private static String generateTxnId() {
        return "TXN-" + UUID.randomUUID().toString().substring(0, 12).toUpperCase();
    }

    public boolean isSuccess() { return success; }
    public String getTransactionId() { return transactionId; }
    public String getMessage() { return message; }

    @Override
    public String toString() {
        if (success) {
            return "SUCCESS | txnId: " + transactionId + " | " + message;
        }
        return "FAILED | " + message;
    }
}

ValidationResult.java

package com.nbi.payment.model;

import java.util.ArrayList;
import java.util.List;

public class ValidationResult {
    private final boolean valid;
    private final List<String> errors;

    private ValidationResult(boolean valid, List<String> errors) {
        this.valid = valid;
        this.errors = errors;
    }

    public static ValidationResult ok() {
        return new ValidationResult(true, List.of());
    }

    public static ValidationResult fail(List<String> errors) {
        return new ValidationResult(false, errors);
    }

    public boolean isValid() { return valid; }
    public List<String> getErrors() { return errors; }
}

PaymentProcessor.java (interface)

package com.nbi.payment.processor;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;

public interface PaymentProcessor {
    ValidationResult validate(PaymentRequest request);
    PaymentResult process(PaymentRequest request);
    String getProviderName();
}

UPIProcessor.java

package com.nbi.payment.processor;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class UPIProcessor implements PaymentProcessor {
    private static final BigDecimal MAX_UPI_AMOUNT = new BigDecimal("100000");

    @Override
    public ValidationResult validate(PaymentRequest request) {
        List<String> errors = new ArrayList<>();
        String upiId = request.getMetadata("upiId");

        if (upiId == null || upiId.isEmpty()) {
            errors.add("UPI ID is required");
        } else if (!upiId.contains("@")) {
            errors.add("Invalid UPI ID format. Expected: name@bank");
        }

        if (request.getAmount().compareTo(MAX_UPI_AMOUNT) > 0) {
            errors.add("UPI limit exceeded. Max: " + MAX_UPI_AMOUNT);
        }

        if (request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            errors.add("Amount must be greater than zero");
        }

        return errors.isEmpty() ? ValidationResult.ok() : ValidationResult.fail(errors);
    }

    @Override
    public PaymentResult process(PaymentRequest request) {
        System.out.println("  [UPI] Connecting to NPCI UPI gateway...");
        System.out.println("  [UPI] Sending payment request to "
            + request.getMetadata("upiId"));
        System.out.println("  [UPI] Waiting for user approval on UPI app...");
        System.out.println("  [UPI] Payment approved. Debiting account.");
        return PaymentResult.success(
            "UPI payment of " + request.getCurrency() + " "
            + request.getAmount() + " completed via "
            + request.getMetadata("upiId"));
    }

    @Override
    public String getProviderName() {
        return "UPI (NPCI)";
    }
}

CreditCardProcessor.java

package com.nbi.payment.processor;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;
import java.util.ArrayList;
import java.util.List;

public class CreditCardProcessor implements PaymentProcessor {

    @Override
    public ValidationResult validate(PaymentRequest request) {
        List<String> errors = new ArrayList<>();
        String cardNumber = request.getMetadata("cardNumber");
        String cvv = request.getMetadata("cvv");
        String expiry = request.getMetadata("expiry");

        if (cardNumber == null || cardNumber.length() != 16) {
            errors.add("Card number must be 16 digits");
        }
        if (cvv == null || cvv.length() != 3) {
            errors.add("CVV must be 3 digits");
        }
        if (expiry == null || expiry.isEmpty()) {
            errors.add("Expiry date is required (MM/YY)");
        }
        if (request.getAmount().compareTo(java.math.BigDecimal.ZERO) <= 0) {
            errors.add("Amount must be greater than zero");
        }

        return errors.isEmpty() ? ValidationResult.ok() : ValidationResult.fail(errors);
    }

    @Override
    public PaymentResult process(PaymentRequest request) {
        String maskedCard = "****-****-****-"
            + request.getMetadata("cardNumber").substring(12);
        System.out.println("  [CARD] Connecting to card network (Visa/MasterCard)...");
        System.out.println("  [CARD] Authorizing card " + maskedCard);
        System.out.println("  [CARD] Charge authorized. Processing settlement.");
        return PaymentResult.success(
            "Credit card payment of " + request.getCurrency() + " "
            + request.getAmount() + " charged to " + maskedCard);
    }

    @Override
    public String getProviderName() {
        return "Credit Card (Visa/MasterCard)";
    }
}

NetBankingProcessor.java

package com.nbi.payment.processor;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;
import java.util.ArrayList;
import java.util.List;

public class NetBankingProcessor implements PaymentProcessor {

    @Override
    public ValidationResult validate(PaymentRequest request) {
        List<String> errors = new ArrayList<>();
        String bankCode = request.getMetadata("bankCode");

        if (bankCode == null || bankCode.isEmpty()) {
            errors.add("Bank code is required");
        }
        if (request.getAmount().compareTo(java.math.BigDecimal.ZERO) <= 0) {
            errors.add("Amount must be greater than zero");
        }

        return errors.isEmpty() ? ValidationResult.ok() : ValidationResult.fail(errors);
    }

    @Override
    public PaymentResult process(PaymentRequest request) {
        System.out.println("  [NETBANK] Redirecting to "
            + request.getMetadata("bankCode") + " net banking portal...");
        System.out.println("  [NETBANK] User logged in and approved transaction.");
        System.out.println("  [NETBANK] Bank confirmed debit. Settlement initiated.");
        return PaymentResult.success(
            "Net Banking payment of " + request.getCurrency() + " "
            + request.getAmount() + " via " + request.getMetadata("bankCode"));
    }

    @Override
    public String getProviderName() {
        return "Net Banking";
    }
}

PaymentProcessorFactory.java (Simple Factory)

package com.nbi.payment.factory;

import com.nbi.payment.processor.PaymentProcessor;
import com.nbi.payment.processor.UPIProcessor;
import com.nbi.payment.processor.CreditCardProcessor;
import com.nbi.payment.processor.NetBankingProcessor;

public class PaymentProcessorFactory {

    /**
     * Creates the appropriate payment processor based on the type string.
     * To add a new payment method: create a new Processor class implementing
     * PaymentProcessor, and add one else-if here. No client code changes.
     */
    public static PaymentProcessor createProcessor(String type) {
        if (type == null) {
            throw new IllegalArgumentException("Payment type cannot be null");
        }

        switch (type.toLowerCase()) {
            case "upi":
                return new UPIProcessor();
            case "card":
            case "creditcard":
                return new CreditCardProcessor();
            case "netbanking":
            case "netbank":
                return new NetBankingProcessor();
            default:
                throw new IllegalArgumentException(
                    "Unsupported payment type: " + type
                    + ". Supported: upi, card, netbanking");
        }
    }
}

PaymentService.java

package com.nbi.payment.service;

import com.nbi.payment.factory.PaymentProcessorFactory;
import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;
import com.nbi.payment.processor.PaymentProcessor;

public class PaymentService {

    public PaymentResult processPayment(PaymentRequest request) {
        System.out.println("Processing: " + request);

        // Step 1: Create the correct processor via factory
        PaymentProcessor processor =
            PaymentProcessorFactory.createProcessor(request.getType());
        System.out.println("Provider: " + processor.getProviderName());

        // Step 2: Validate
        ValidationResult validation = processor.validate(request);
        if (!validation.isValid()) {
            System.out.println("Validation failed: " + validation.getErrors());
            return PaymentResult.failure(
                "Validation failed: " + String.join(", ", validation.getErrors()));
        }
        System.out.println("Validation passed.");

        // Step 3: Process
        PaymentResult result = processor.process(request);
        System.out.println("Result: " + result);

        return result;
    }
}

Application.java

package com.nbi.payment;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.service.PaymentService;
import java.math.BigDecimal;

public class Application {
    public static void main(String[] args) {
        PaymentService service = new PaymentService();

        // Scenario 1: UPI Payment
        System.out.println("=== Scenario 1: UPI Payment ===");
        PaymentRequest upiRequest = new PaymentRequest("upi", new BigDecimal("5000"))
            .withMetadata("upiId", "rajesh@icici");
        PaymentResult upiResult = service.processPayment(upiRequest);
        System.out.println();

        // Scenario 2: Credit Card Payment
        System.out.println("=== Scenario 2: Credit Card Payment ===");
        PaymentRequest cardRequest = new PaymentRequest("card", new BigDecimal("25000"))
            .withMetadata("cardNumber", "4111222233334444")
            .withMetadata("cvv", "123")
            .withMetadata("expiry", "12/28");
        PaymentResult cardResult = service.processPayment(cardRequest);
        System.out.println();

        // Scenario 3: Net Banking Payment
        System.out.println("=== Scenario 3: Net Banking Payment ===");
        PaymentRequest netbankRequest = new PaymentRequest("netbanking", new BigDecimal("150000"))
            .withMetadata("bankCode", "SBI");
        PaymentResult netbankResult = service.processPayment(netbankRequest);
        System.out.println();

        // Scenario 4: Validation Failure (UPI without UPI ID)
        System.out.println("=== Scenario 4: Validation Failure ===");
        PaymentRequest badRequest = new PaymentRequest("upi", new BigDecimal("5000"));
        PaymentResult badResult = service.processPayment(badRequest);
        System.out.println();

        // Scenario 5: Invalid Payment Type
        System.out.println("=== Scenario 5: Invalid Payment Type ===");
        try {
            PaymentRequest invalidRequest = new PaymentRequest("bitcoin", new BigDecimal("1000"));
            service.processPayment(invalidRequest);
        } catch (IllegalArgumentException e) {
            System.out.println("Error: " + e.getMessage());
        }
        System.out.println();

        // Scenario 6: UPI Amount Limit Exceeded
        System.out.println("=== Scenario 6: UPI Limit Exceeded ===");
        PaymentRequest limitRequest = new PaymentRequest("upi", new BigDecimal("200000"))
            .withMetadata("upiId", "priya@hdfc");
        PaymentResult limitResult = service.processPayment(limitRequest);
    }
}

Expected Output

=== Scenario 1: UPI Payment === Processing: UPI payment of INR 5000 Provider: UPI (NPCI) Validation passed. [UPI] Connecting to NPCI UPI gateway... [UPI] Sending payment request to rajesh@icici [UPI] Waiting for user approval on UPI app... [UPI] Payment approved. Debiting account. Result: SUCCESS | txnId: TXN-A1B2C3D4E5F6 | UPI payment of INR 5000 completed via rajesh@icici === Scenario 2: Credit Card Payment === Processing: CARD payment of INR 25000 Provider: Credit Card (Visa/MasterCard) Validation passed. [CARD] Connecting to card network (Visa/MasterCard)... [CARD] Authorizing card ****-****-****-4444 [CARD] Charge authorized. Processing settlement. Result: SUCCESS | txnId: TXN-G7H8I9J0K1L2 | Credit card payment of INR 25000 charged to ****-****-****-4444 === Scenario 3: Net Banking Payment === Processing: NETBANKING payment of INR 150000 Provider: Net Banking Validation passed. [NETBANK] Redirecting to SBI net banking portal... [NETBANK] User logged in and approved transaction. [NETBANK] Bank confirmed debit. Settlement initiated. Result: SUCCESS | txnId: TXN-M3N4O5P6Q7R8 | Net Banking payment of INR 150000 via SBI === Scenario 4: Validation Failure === Processing: UPI payment of INR 5000 Provider: UPI (NPCI) Validation failed: [UPI ID is required] Result: FAILED | Validation failed: UPI ID is required === Scenario 5: Invalid Payment Type === Error: Unsupported payment type: bitcoin. Supported: upi, card, netbanking === Scenario 6: UPI Limit Exceeded === Processing: UPI payment of INR 200000 Provider: UPI (NPCI) Validation failed: [UPI limit exceeded. Max: 100000] Result: FAILED | Validation failed: UPI limit exceeded. Max: 100000

PaymentProcessorFactoryTest.java

package com.nbi.payment.factory;

import com.nbi.payment.processor.PaymentProcessor;
import com.nbi.payment.processor.UPIProcessor;
import com.nbi.payment.processor.CreditCardProcessor;
import com.nbi.payment.processor.NetBankingProcessor;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class PaymentProcessorFactoryTest {

    @Test
    void createProcessor_returnsUPIProcessor() {
        PaymentProcessor processor = PaymentProcessorFactory.createProcessor("upi");
        assertTrue(processor instanceof UPIProcessor);
    }

    @Test
    void createProcessor_returnsCreditCardProcessor() {
        PaymentProcessor processor = PaymentProcessorFactory.createProcessor("card");
        assertTrue(processor instanceof CreditCardProcessor);
    }

    @Test
    void createProcessor_returnsNetBankingProcessor() {
        PaymentProcessor processor = PaymentProcessorFactory.createProcessor("netbanking");
        assertTrue(processor instanceof NetBankingProcessor);
    }

    @Test
    void createProcessor_isCaseInsensitive() {
        PaymentProcessor p1 = PaymentProcessorFactory.createProcessor("UPI");
        PaymentProcessor p2 = PaymentProcessorFactory.createProcessor("upi");
        assertEquals(p1.getProviderName(), p2.getProviderName());
    }

    @Test
    void createProcessor_throwsForNull() {
        assertThrows(IllegalArgumentException.class,
            () -> PaymentProcessorFactory.createProcessor(null));
    }

    @Test
    void createProcessor_throwsForInvalidType() {
        IllegalArgumentException ex = assertThrows(
            IllegalArgumentException.class,
            () -> PaymentProcessorFactory.createProcessor("bitcoin"));
        assertTrue(ex.getMessage().contains("Unsupported payment type"));
    }

    @Test
    void createProcessor_acceptsAlias() {
        PaymentProcessor p = PaymentProcessorFactory.createProcessor("creditcard");
        assertTrue(p instanceof CreditCardProcessor);
    }

    @Test
    void eachCallCreatesNewInstance() {
        PaymentProcessor p1 = PaymentProcessorFactory.createProcessor("upi");
        PaymentProcessor p2 = PaymentProcessorFactory.createProcessor("upi");
        assertNotSame(p1, p2);
    }
}

Key observations from the project

  1. Loose coupling: PaymentService never imports UPIProcessor, CreditCardProcessor, or NetBankingProcessor. It only knows the PaymentProcessor interface and the factory.
  2. Open for extension: To add a Debit Card processor, create DebitCardProcessor implements PaymentProcessor and add one case to the factory switch. No other file changes.
  3. Each processor validates independently: UPI checks for UPI ID format and amount limit. Credit Card checks for 16-digit number and CVV. Net Banking checks for bank code. The service does not know the rules.
  4. Six scenarios tested: Happy path (UPI, Card, NetBanking), validation failure, invalid type, limit exceeded. All demonstrate factory behavior.
  5. Follows Single Responsibility: Factory creates objects. Processors validate and process. Service orchestrates the flow. Each class has one job.

10. When to Use and When NOT to Use

When to use Factory Pattern

ScenarioExampleWhy Factory
Object creation depends on inputPayment type determines which processor to createClient should not decide; factory centralizes the decision
Multiple implementations of an interfaceJDBC vs Hibernate DAOFactory selects the correct implementation at runtime
Complex object creationCreating a car involves engine, body, interior assemblyFactory hides the complexity from the client
Object creation may changeToday we use MySQL, tomorrow PostgreSQLChange only the factory, not every client
Framework/library designSpring BeanFactory, JDBC DriverManagerUsers get objects without knowing internal creation details

When NOT to use Factory Pattern

ScenarioWhy NOTUse Instead
Single concrete classNo need for abstraction when there is only one implementationDirect instantiation or Singleton
Simple object, no variantsFactory adds unnecessary complexity for trivial objectsnew ClassName()
Object created once at startupFactory is for repeated creation decisions, not one-time setupBuilder or Singleton
Performance-critical hot pathFactory method call adds indirection (negligible in most cases, but matters in tight loops)Direct instantiation

11. Quick Reference Card

Factory Pattern Comparison

AspectSimple FactoryFactory MethodAbstract Factory
GOF Pattern?No (informal pattern)YesYes
Class count1 factory + N products1 abstract + N factories + N products1 abstract + N factory families + N product families
ExtensionModify factory (if/else)Add new factory subclassAdd new factory family
OCP compliantNo (must modify factory)Yes (add subclass)Yes (add family)
Best forSmall projects, few typesMultiple factories, same productsMultiple product families
JDK exampleCalendar.getInstance()Collection.iterator()javax.xml.parsers.DocumentBuilderFactory
Creational GOF Pattern Medium

Factory and Factory Method Design Pattern in Java

Complete Tutorial | 13 Sections | Full Payment Gateway Project with HLD and LLD | Simple Factory vs Factory Method vs Abstract Factory | Multiple Scenarios | 15 Interview Questions

1. What is Factory Pattern?

Definition

The Factory Pattern is a GOF Creational Pattern that provides a way to create objects without exposing the creation logic to the client. The client refers to newly created objects using a common interface or base class, not the concrete class.

One line: A factory class internally contains a factory method that creates and returns one of several related class objects based on the data we pass.

Sometimes we have a class, but do not know how to create the object for that class because objects can be created in different ways: using new operator, using factory method, using reflection API, etc. In such situations, instead of the client creating the object itself, we delegate the creation to a Factory class that hides the details.

Real-world examples

  • CarFactory: Gives a car based on the model type you pass, without the customer knowing how the car is manufactured.
  • Bakery: Gives a cake based on the type you request, without the customer knowing how the cake is prepared.
  • DriverManager.getConnection(...): Gives an Oracle JDBC connection object if the URL has Oracle details, or a MySQL JDBC connection object if the URL has MySQL details. The client does not worry about how the connection is created.
  • Spring Framework: factory.getBean(...) is a factory pattern. Spring provides abstraction on bean class object creation and returns the object to the client when needed.

Factory Pattern has two parts

  1. Factory Pattern internally contains one static factory method or non-static method and provides abstraction on the object creation process.
  2. Factory Pattern creates and returns one of several related class objects based on the data we have passed.

Pre-defined Factory Methods in JDK

Factory MethodStatic / Non-StaticReturns
Thread.currentThread()StaticReturns its own class object (Thread)
Class.forName("java.util.Date")StaticReturns its own class object (Class)
Calendar.getInstance()StaticReturns related class object (GregorianCalendar subclass)
Properties.getProperties()StaticReturns related class object (between System class and Properties class)
DriverManager.getConnection(url, user, pwd)StaticReturns related class object (driver-specific Connection)
con.createStatement()Non-StaticReturns related class object (called on Connection object)
st.executeQuery()Non-StaticReturns related class object (ResultSet)
Note

In fact, most of the JDBC methods like con.createStatement(), st.executeQuery(), and others are factory methods. They create and return objects without exposing how the underlying driver creates them.

2. The Problem: Tight Coupling

Problem

When the client directly creates objects of concrete classes using new, it leads to:

  • Every Client/Customer must know how to create the object. If the object has dependent objects, they must also know how to create those and add them to the main object.
  • Creating multiple objects for multiple related classes and using one/few of them is bad design.
  • If a concrete class name changes, every client must be modified and recompiled.
  • Object creation logic is scattered and duplicated across all clients.

Problem Code: Car hierarchy without factory

Car.java (abstract base class)

package com.nt.comps;

public abstract class Car {
    private int engineId;
    private String regNo;

    public abstract void assemble();
    public abstract void roadTest();
    public abstract void drive();

    // getters and setters
    public int getEngineId() { return engineId; }
    public void setEngineId(int engineId) { this.engineId = engineId; }
    public String getRegNo() { return regNo; }
    public void setRegNo(String regNo) { this.regNo = regNo; }
}

SportsCar.java

package com.nt.comps;

public class SportsCar extends Car {
    private String speed;
    private String pickup;

    public SportsCar() {
        System.out.println("SportsCar:: 0-param constructor");
    }

    @Override
    public void assemble() {
        System.out.println("SportsCar::assemble()");
    }

    @Override
    public void roadTest() {
        System.out.println("SportsCar::roadTest()");
    }

    @Override
    public void drive() {
        System.out.println("SportsCar::drive()");
    }
}

LuxuryCar.java

package com.nt.comps;

public class LuxuryCar extends Car {
    private String bootspace;
    private String security;

    public LuxuryCar() {
        System.out.println("LuxuryCar:: 0-param constructor");
    }

    @Override
    public void assemble() {
        System.out.println("LuxuryCar::assemble()");
    }

    @Override
    public void roadTest() {
        System.out.println("LuxuryCar::roadTest()");
    }

    @Override
    public void drive() {
        System.out.println("LuxuryCar::drive()");
    }
}

BudgetCar.java

package com.nt.comps;

public class BudgetCar extends Car {
    private String fuelEconomy;
    private float price;

    public BudgetCar() {
        System.out.println("BudgetCar:: 0-param constructor");
    }

    @Override
    public void assemble() {
        System.out.println("BudgetCar::assemble()");
    }

    @Override
    public void roadTest() {
        System.out.println("BudgetCar::roadTest()");
    }

    @Override
    public void drive() {
        System.out.println("BudgetCar::drive()");
    }
}

Problem client classes (tightly coupled)

// CarCustomer1.java - client must import and know SportsCar
package com.nt.test;

import com.nt.comps.Car;
import com.nt.comps.SportsCar;

public class CarCustomer1 {
    public static void main(String[] args) {
        Car sCar = new SportsCar();  // tightly coupled to SportsCar
        sCar.assemble();
        sCar.roadTest();
        sCar.drive();
    }
}

// CarCustomer2.java - client must import and know BudgetCar
package com.nt.test;

import com.nt.comps.BudgetCar;
import com.nt.comps.Car;

public class CarCustomer2 {
    public static void main(String[] args) {
        Car bCar = new BudgetCar();  // tightly coupled to BudgetCar
        bCar.assemble();
        bCar.roadTest();
        bCar.drive();
    }
}

// CarCustomer3.java - client must import and know LuxuryCar
package com.nt.test;

import com.nt.comps.Car;
import com.nt.comps.LuxuryCar;

public class CarCustomer3 {
    public static void main(String[] args) {
        Car lCar = new LuxuryCar();  // tightly coupled to LuxuryCar
        lCar.assemble();
        lCar.roadTest();
        lCar.drive();
    }
}
Why is this bad?

Every Client/Customer must know to create Car object. If Car object is having any dependent objects, they should also know how to create those dependent objects and how to add those objects to the main object. Creating multiple objects for multiple related classes and using one/few of them is bad thought.

To overcome these problems we need to add a Factory Pattern class which contains one factory method taking care of object creation process by providing abstraction from customers/clients.

3. The Solution: Simple Factory

Solution

Move the object creation logic into a separate Factory class. The client only knows the base class (Car) and the factory. It passes a car type String; the factory decides which concrete class to instantiate. The client never uses new on concrete classes and never imports them.

CarFactory.java

package com.nt.factory;

import com.nt.comps.Car;
import com.nt.comps.SportsCar;
import com.nt.comps.LuxuryCar;
import com.nt.comps.BudgetCar;

public class CarFactory {

    // STATIC factory method
    public static Car createCar(String carType) {
        if (carType == null) {
            throw new IllegalArgumentException("car type cannot be null");
        }

        Car car;
        if (carType.equalsIgnoreCase("sports")) {
            car = new SportsCar();
        } else if (carType.equalsIgnoreCase("luxury")) {
            car = new LuxuryCar();
        } else if (carType.equalsIgnoreCase("budget")) {
            car = new BudgetCar();
        } else {
            throw new IllegalArgumentException("Invalid car type: " + carType);
        }
        return car;
    }
}

Solution client classes (loosely coupled)

// CarCustomer1.java - client knows only Car + CarFactory
package com.nt.test;

import com.nt.comps.Car;
import com.nt.factory.CarFactory;

public class CarCustomer1 {
    public static void main(String[] args) {
        // client is loosely coupled - no concrete class imported
        Car car = CarFactory.createCar("sports");
        car.assemble();
        car.roadTest();
        car.drive();
    }
}

// CarCustomer2.java - same factory, different type
package com.nt.test;

import com.nt.comps.Car;
import com.nt.factory.CarFactory;

public class CarCustomer2 {
    public static void main(String[] args) {
        Car car = CarFactory.createCar("luxury");
        car.assemble();
        car.roadTest();
        car.drive();
    }
}

// Adding a new car type now requires a change ONLY in CarFactory,
// not in any client class.
SportsCar:: 0-param constructor SportsCar::assemble() SportsCar::roadTest() SportsCar::drive()
Key Benefit

Note that the client imports com.nt.comps.Car (the base class) and com.nt.factory.CarFactory (the factory). It does NOT import SportsCar, LuxuryCar, or BudgetCar. If a new car type is added or a class is renamed, only CarFactory changes. The factory is now developed by another developer. That "other one" we pass the description of the object to in the factory method.

4. What is a Factory Method?

Definition

The method that is capable of creating and returning either its own class object, or a related class object, or an unrelated class object (in fact capable of returning any object) is called a factory method.

Two types of factory methods

1. Static factory method

Invokable on the class name directly, without creating an object first.

// Returns its own class object
Thread t = Thread.currentThread();
Class c = Class.forName("java.util.Date");

// Returns related class object (subclass)
Calendar cal = Calendar.getInstance();
// returns GregorianCalendar which is a subclass of Calendar

// Returns related class object (JDBC)
Connection con = DriverManager.getConnection("oracle_url", "user", "pwd");
// oracle con obj

Connection con = DriverManager.getConnection("mysql_url", "user", "pwd");
// mysql con obj

2. Non-static factory method

Invokable on a Java class object. Requires creating an instance first.

// Returns same class object
String s = new String("a1");
String s2 = s.concat("hello");  // returns same class obj

// Returns unrelated class object
StringBuffer sb = new StringBuffer("hello how are u");
String s3 = sb.substring(0, 5);  // returns String (unrelated)

// Returns related class object
CarFactory factory = new CarFactory();
Car car1 = factory.getCar("budget");  // gives related class obj
// BudgetCar extends Car

5. Factory Method vs Factory Pattern

Key Distinction

Factory Method creates and returns an object. That object can be the same class, a related class, or an unrelated class.

Factory Pattern contains a factory method but it always returns one of several related class objects based on the input. The pattern is about the class structure, not just the method.

AspectFactory MethodFactory Pattern
ScopeA single method that creates and returns an objectA class structure that uses a factory method to create related objects
Return typeCan return any Java class objectAlways returns one of several related class objects
ExamplesProperties.getProperty() is a factory method but not a factory patternDriverManager.getConnection() is a factory method AND uses factory pattern

Conclusion: Factory method can return any Java class object. Factory pattern returns one of several related classes. Every factory pattern uses a factory method, but not every factory method is a factory pattern.

6. Three Types of Factory Design Patterns

All three patterns use a factory method. The difference is in structure and flexibility:

PatternDescriptionStructureWhen to Use
Simple FactoryOne factory class, one method, creates objects via if/else or switch1 factory class, many productsSimple object creation, few product types
Factory MethodAbstract factory class defines the creation method. Subclasses decide which class to instantiate1 abstract factory, many concrete factory subclassesMultiple factories for same product family, each with its own logic
Abstract FactoryFactory of factories. Provides interface for creating families of related objectsFactory that returns other factoriesMultiple product families (e.g., JDBC vs Hibernate)
Important

"Factory method" in Java terminology means a Java method which creates and returns an object. "Factory method pattern" is a design pattern which internally uses a factory pattern with factory method. Do not confuse the two.

Relationship: Singleton class allows to create only one object. Factory Pattern provides abstraction on object creation.

7. Factory Method Pattern: Bajaj Bike Example

Scenario

Bajaj has multiple manufacturing factories (Chennai for South customers, Noida for North customers). Both factories produce the same family of bikes (Pulsor, Discover) but each factory follows its own manufacturing standards. We need a common ordering process while letting each factory decide how to create the bike.

Analogy: Factory Method pattern is like RBI for banks, TRAI for Telecom companies, IRDA for insurance companies. It is the super class for the factories who are creating objects of same family classes, providing standards to them toward object creation.

Class diagram

BajajFactory (abstract) - abstract painting() - abstract assembling() - abstract engineTest() - abstract roadTest() - abstract createBike(String type) [Factory Method] + public BajajBike orderBike(String type) [Template Method] | +-----------+-----------+ | | ChennaiBajajFactory NoidaBajajFactory (for South customers) (for North customers) - createBike() - createBike() - painting() - painting() - assembling() - assembling() Products (com.nt.bike): BajajBike (abstract base) | +--------+---------+ | | PulsorBike DiscoverBike

BajajBike.java (base product)

package com.nt.bike;

public abstract class BajajBike {
    public abstract void bikeInfo();
}

PulsorBike.java

package com.nt.bike;

public class PulsorBike extends BajajBike {
    @Override
    public void bikeInfo() {
        System.out.println("PulsorBike :: sporty bike");
    }
}

DiscoverBike.java

package com.nt.bike;

public class DiscoverBike extends BajajBike {
    @Override
    public void bikeInfo() {
        System.out.println("DiscoverBike :: commuter bike");
    }
}

BajajFactory.java (abstract factory with factory method + template method)

package com.nt.factory;

import com.nt.bike.BajajBike;

public abstract class BajajFactory {

    // Method declarations - subclasses implement these
    protected abstract void painting();
    protected abstract void assembling();
    protected abstract void engineTest();
    protected abstract void roadTest();

    // FACTORY METHOD - subclasses decide which concrete bike to create
    public abstract BajajBike createBike(String bikeType);

    // TEMPLATE METHOD - fixed order, uses factory method
    public final BajajBike orderBike(String bikeType) {
        BajajBike bike = createBike(bikeType);  // created by subclass
        painting();
        assembling();
        engineTest();
        roadTest();
        return bike;
    }
}

ChennaiBajajFactory.java (for South customers)

package com.nt.factory;

import com.nt.bike.BajajBike;
import com.nt.bike.PulsorBike;
import com.nt.bike.DiscoverBike;

public class ChennaiBajajFactory extends BajajFactory {

    @Override
    public BajajBike createBike(String bikeType) {
        System.out.println("Chennai plant creating bike...");
        if (bikeType.equalsIgnoreCase("pulsor")) return new PulsorBike();
        if (bikeType.equalsIgnoreCase("discover")) return new DiscoverBike();
        throw new IllegalArgumentException("Invalid bike type: " + bikeType);
    }

    @Override protected void painting() { System.out.println("Chennai: painting"); }
    @Override protected void assembling() { System.out.println("Chennai: assembling"); }
    @Override protected void engineTest() { System.out.println("Chennai: engine test"); }
    @Override protected void roadTest() { System.out.println("Chennai: road test"); }
}

NoidaBajajFactory.java (for North customers)

package com.nt.factory;

import com.nt.bike.BajajBike;
import com.nt.bike.PulsorBike;
import com.nt.bike.DiscoverBike;

public class NoidaBajajFactory extends BajajFactory {

    @Override
    public BajajBike createBike(String bikeType) {
        System.out.println("Noida plant creating bike...");
        if (bikeType.equalsIgnoreCase("pulsor")) return new PulsorBike();
        if (bikeType.equalsIgnoreCase("discover")) return new DiscoverBike();
        throw new IllegalArgumentException("Invalid bike type: " + bikeType);
    }

    @Override protected void painting() { System.out.println("Noida: painting"); }
    @Override protected void assembling() { System.out.println("Noida: assembling"); }
    @Override protected void engineTest() { System.out.println("Noida: engine test"); }
    @Override protected void roadTest() { System.out.println("Noida: road test"); }
}

Client classes

// SouthCustomer.java
package com.nt.test;

import com.nt.bike.BajajBike;
import com.nt.factory.BajajFactory;
import com.nt.factory.ChennaiBajajFactory;

public class SouthCustomer {
    public static void main(String[] args) {
        BajajFactory factory = new ChennaiBajajFactory();
        BajajBike bike = factory.orderBike("pulsor");
        bike.bikeInfo();
    }
}

// NorthCustomer.java
package com.nt.test;

import com.nt.bike.BajajBike;
import com.nt.factory.BajajFactory;
import com.nt.factory.NoidaBajajFactory;

public class NorthCustomer {
    public static void main(String[] args) {
        BajajFactory factory = new NoidaBajajFactory();
        BajajBike bike = factory.orderBike("discover");
        bike.bikeInfo();
    }
}
// SouthCustomer output: Chennai plant creating bike... Chennai: painting Chennai: assembling Chennai: engine test Chennai: road test PulsorBike :: sporty bike // NorthCustomer output: Noida plant creating bike... Noida: painting Noida: assembling Noida: engine test Noida: road test DiscoverBike :: commuter bike
Key Point

Here both factories are creating same Bajaj bike family class objects. Since both factories are independent factories, they are following their own standards while manufacturing bikes (creating objects). Due to this, quality differences are coming in the same family (same company) bikes.

The abstract BajajFactory fixes the manufacturing algorithm (orderBike = create + paint + assemble + engineTest + roadTest). Only the createBike() factory method varies per plant. The client picks a factory and calls orderBike() without knowing which concrete bike class is produced.

8. Abstract Factory: Factory of Factories

Definition

Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is a factory of factories.

DAO Example: Switching between JDBC and Hibernate

ApplicationFactory (Factory of Factories) | DAOFactory (Abstract) / \ JdbcDAOFactory HibernateDAOFactory | | EmployeeJdbc EmployeeHibernate DAOImpl DAOImpl
// DAO Interface
public interface IEmployeeDAO {
    void insert(EmployeeBO bo);
    EmployeeBO find(int empno);
}

// JDBC Implementation
public class EmployeeJdbcDAOImpl implements IEmployeeDAO {
    public void insert(EmployeeBO bo) { /* JDBC logic */ }
    public EmployeeBO find(int empno) { /* JDBC logic */ return null; }
}

// Hibernate Implementation
public class EmployeeHibernateDAOImpl implements IEmployeeDAO {
    public void insert(EmployeeBO bo) { /* Hibernate logic */ }
    public EmployeeBO find(int empno) { /* Hibernate logic */ return null; }
}

// Abstract Factory
public abstract class DAOFactory {
    public abstract IEmployeeDAO getEmployeeDAO();

    public static DAOFactory getInstance(String type) {
        if ("jdbc".equals(type)) return new JdbcDAOFactory();
        if ("hibernate".equals(type)) return new HibernateDAOFactory();
        throw new IllegalArgumentException("Unknown DAO type: " + type);
    }
}

// Concrete Factories
public class JdbcDAOFactory extends DAOFactory {
    public IEmployeeDAO getEmployeeDAO() {
        return new EmployeeJdbcDAOImpl();
    }
}

public class HibernateDAOFactory extends DAOFactory {
    public IEmployeeDAO getEmployeeDAO() {
        return new EmployeeHibernateDAOImpl();
    }
}

// ApplicationFactory (factory of factories)
public class ApplicationFactory {
    public static DAOFactory getDAOFactory(String type) {
        return DAOFactory.getInstance(type);
    }
}

All Three Patterns Compared

AspectSimple FactoryFactory MethodAbstract Factory
StructureOne factory class creates all objectsAbstract factory, concrete factory subclasses decide which class to instantiateFactory of factories, creates families of related objects
Approachif/else or switch in one methodInheritance-based (subclass overrides factory method)Composition-based (factory returns other factories)
ProductsOne product type, multiple variantsOne product type, factory-specific creationMultiple product types, family-based
FlexibilityLow (adding types requires changing factory)Medium (new factory subclass for new logic)High (new family = new factory, no changes to existing)
Use whenFew product types, simple creationEach factory needs different creation logicMultiple product families (JDBC vs Hibernate, UI themes)

9. Full-Length Industry Project: Payment Gateway System

Business Scenario

National Bank of India is building an online payment system. Customers can pay using UPI, Credit Card, or Net Banking. Each payment method has different validation rules, processing logic, and confirmation flows. The bank wants to add new payment methods (Debit Card, Wallet) in the future without changing existing code. The system must be loosely coupled so that adding a new payment method requires only adding new classes, not modifying existing ones.

High-Level Design (HLD)

+------------------+ | Client Tier | | Browser/Mobile | +--------+---------+ | v +------------------------------------------------------+ | CONTROLLER LAYER | | PaymentController | | - receives payment request with type and amount | | - delegates to PaymentService | +----------------------------+-------------------------+ | v +------------------------------------------------------+ | SERVICE LAYER | | PaymentService | | - uses PaymentProcessorFactory to get processor | | - validates, processes, confirms | +----------------------------+-------------------------+ | v +------------------------------------------------------+ | FACTORY LAYER (Factory Pattern) | | | | PaymentProcessorFactory | | +-- createProcessor("upi") --> UPIProcessor | | +-- createProcessor("card") --> CreditCardProc | | +-- createProcessor("netbank") --> NetBankingProc | +------------------------------------------------------+ | v +------------------------------------------------------+ | PROCESSOR LAYER (Products) | | | | PaymentProcessor (interface) | | | | | +-- UPIProcessor | | +-- CreditCardProcessor | | +-- NetBankingProcessor | | +-- (future: DebitCardProcessor, WalletProc...) | +------------------------------------------------------+ | v +------------------------------------------------------+ | EXTERNAL SYSTEMS | | UPI Gateway | Card Network | Bank Server | +------------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | PaymentProcessor (interface) | +-------------------------------------------------------+ | + validate(request): ValidationResult | | + process(request): PaymentResult | | + getProviderName(): String | +-------------------------------------------------------+ ^ ^ ^ | | | +----------------+ +------------------+ +-------------------+ | UPIProcessor | | CreditCardProc | | NetBankingProc | +----------------+ +------------------+ +-------------------+ | - upiId: String| | - cardNumber | | - bankCode | | | | - cvv | | - accountNumber | +----------------+ +------------------+ +-------------------+ | + validate() | | + validate() | | + validate() | | + process() | | + process() | | + process() | | + getProvider()| | + getProvider() | | + getProvider() | +----------------+ +------------------+ +-------------------+ +-------------------------------------------------------+ | PaymentProcessorFactory (Simple Factory) | +-------------------------------------------------------+ | + createProcessor(type): PaymentProcessor [static] | +-------------------------------------------------------+ | Reads type string, returns correct processor. | | New type = add one else-if. No client changes. | +-------------------------------------------------------+ +-------------------------------------------------------+ | PaymentService | +-------------------------------------------------------+ | - factory: PaymentProcessorFactory | | - logger: (prints to console) | +-------------------------------------------------------+ | + processPayment(request): PaymentResult | +-------------------------------------------------------+ +-------------------------------------------------------+ | Model Classes | +-------------------------------------------------------+ | PaymentRequest: type, amount, currency, metadata | | PaymentResult: success, transactionId, message | | ValidationResult: valid, errors | +-------------------------------------------------------+

Project Structure

nbi-payment-gateway/ ├── src/ │ └── main/java/com/nbi/payment/ │ ├── model/ │ │ ├── PaymentRequest.java │ │ ├── PaymentResult.java │ │ └── ValidationResult.java │ ├── processor/ │ │ ├── PaymentProcessor.java (interface) │ │ ├── UPIProcessor.java │ │ ├── CreditCardProcessor.java │ │ └── NetBankingProcessor.java │ ├── factory/ │ │ └── PaymentProcessorFactory.java (simple factory) │ ├── service/ │ │ └── PaymentService.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/payment/ │ ├── factory/ │ │ └── PaymentProcessorFactoryTest.java │ └── service/ │ └── PaymentServiceTest.java └── pom.xml

PaymentRequest.java

package com.nbi.payment.model;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

public class PaymentRequest {
    private final String type;
    private final BigDecimal amount;
    private final String currency;
    private final Map<String, String> metadata;

    public PaymentRequest(String type, BigDecimal amount) {
        this.type = type;
        this.amount = amount;
        this.currency = "INR";
        this.metadata = new HashMap<>();
    }

    public PaymentRequest withMetadata(String key, String value) {
        this.metadata.put(key, value);
        return this;
    }

    public String getType() { return type; }
    public BigDecimal getAmount() { return amount; }
    public String getCurrency() { return currency; }
    public String getMetadata(String key) { return metadata.getOrDefault(key, ""); }

    @Override
    public String toString() {
        return type.toUpperCase() + " payment of " + currency + " " + amount;
    }
}

PaymentResult.java

package com.nbi.payment.model;

import java.util.UUID;

public class PaymentResult {
    private final boolean success;
    private final String transactionId;
    private final String message;

    private PaymentResult(boolean success, String transactionId, String message) {
        this.success = success;
        this.transactionId = transactionId;
        this.message = message;
    }

    public static PaymentResult success(String message) {
        return new PaymentResult(true, generateTxnId(), message);
    }

    public static PaymentResult failure(String message) {
        return new PaymentResult(false, null, message);
    }

    private static String generateTxnId() {
        return "TXN-" + UUID.randomUUID().toString().substring(0, 12).toUpperCase();
    }

    public boolean isSuccess() { return success; }
    public String getTransactionId() { return transactionId; }
    public String getMessage() { return message; }

    @Override
    public String toString() {
        if (success) {
            return "SUCCESS | txnId: " + transactionId + " | " + message;
        }
        return "FAILED | " + message;
    }
}

ValidationResult.java

package com.nbi.payment.model;

import java.util.ArrayList;
import java.util.List;

public class ValidationResult {
    private final boolean valid;
    private final List<String> errors;

    private ValidationResult(boolean valid, List<String> errors) {
        this.valid = valid;
        this.errors = errors;
    }

    public static ValidationResult ok() {
        return new ValidationResult(true, List.of());
    }

    public static ValidationResult fail(List<String> errors) {
        return new ValidationResult(false, errors);
    }

    public boolean isValid() { return valid; }
    public List<String> getErrors() { return errors; }
}

PaymentProcessor.java (interface)

package com.nbi.payment.processor;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;

public interface PaymentProcessor {
    ValidationResult validate(PaymentRequest request);
    PaymentResult process(PaymentRequest request);
    String getProviderName();
}

UPIProcessor.java

package com.nbi.payment.processor;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class UPIProcessor implements PaymentProcessor {
    private static final BigDecimal MAX_UPI_AMOUNT = new BigDecimal("100000");

    @Override
    public ValidationResult validate(PaymentRequest request) {
        List<String> errors = new ArrayList<>();
        String upiId = request.getMetadata("upiId");

        if (upiId == null || upiId.isEmpty()) {
            errors.add("UPI ID is required");
        } else if (!upiId.contains("@")) {
            errors.add("Invalid UPI ID format. Expected: name@bank");
        }

        if (request.getAmount().compareTo(MAX_UPI_AMOUNT) > 0) {
            errors.add("UPI limit exceeded. Max: " + MAX_UPI_AMOUNT);
        }

        if (request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            errors.add("Amount must be greater than zero");
        }

        return errors.isEmpty() ? ValidationResult.ok() : ValidationResult.fail(errors);
    }

    @Override
    public PaymentResult process(PaymentRequest request) {
        System.out.println("  [UPI] Connecting to NPCI UPI gateway...");
        System.out.println("  [UPI] Sending payment request to "
            + request.getMetadata("upiId"));
        System.out.println("  [UPI] Waiting for user approval on UPI app...");
        System.out.println("  [UPI] Payment approved. Debiting account.");
        return PaymentResult.success(
            "UPI payment of " + request.getCurrency() + " "
            + request.getAmount() + " completed via "
            + request.getMetadata("upiId"));
    }

    @Override
    public String getProviderName() {
        return "UPI (NPCI)";
    }
}

CreditCardProcessor.java

package com.nbi.payment.processor;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;
import java.util.ArrayList;
import java.util.List;

public class CreditCardProcessor implements PaymentProcessor {

    @Override
    public ValidationResult validate(PaymentRequest request) {
        List<String> errors = new ArrayList<>();
        String cardNumber = request.getMetadata("cardNumber");
        String cvv = request.getMetadata("cvv");
        String expiry = request.getMetadata("expiry");

        if (cardNumber == null || cardNumber.length() != 16) {
            errors.add("Card number must be 16 digits");
        }
        if (cvv == null || cvv.length() != 3) {
            errors.add("CVV must be 3 digits");
        }
        if (expiry == null || expiry.isEmpty()) {
            errors.add("Expiry date is required (MM/YY)");
        }
        if (request.getAmount().compareTo(java.math.BigDecimal.ZERO) <= 0) {
            errors.add("Amount must be greater than zero");
        }

        return errors.isEmpty() ? ValidationResult.ok() : ValidationResult.fail(errors);
    }

    @Override
    public PaymentResult process(PaymentRequest request) {
        String maskedCard = "****-****-****-"
            + request.getMetadata("cardNumber").substring(12);
        System.out.println("  [CARD] Connecting to card network (Visa/MasterCard)...");
        System.out.println("  [CARD] Authorizing card " + maskedCard);
        System.out.println("  [CARD] Charge authorized. Processing settlement.");
        return PaymentResult.success(
            "Credit card payment of " + request.getCurrency() + " "
            + request.getAmount() + " charged to " + maskedCard);
    }

    @Override
    public String getProviderName() {
        return "Credit Card (Visa/MasterCard)";
    }
}

NetBankingProcessor.java

package com.nbi.payment.processor;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;
import java.util.ArrayList;
import java.util.List;

public class NetBankingProcessor implements PaymentProcessor {

    @Override
    public ValidationResult validate(PaymentRequest request) {
        List<String> errors = new ArrayList<>();
        String bankCode = request.getMetadata("bankCode");

        if (bankCode == null || bankCode.isEmpty()) {
            errors.add("Bank code is required");
        }
        if (request.getAmount().compareTo(java.math.BigDecimal.ZERO) <= 0) {
            errors.add("Amount must be greater than zero");
        }

        return errors.isEmpty() ? ValidationResult.ok() : ValidationResult.fail(errors);
    }

    @Override
    public PaymentResult process(PaymentRequest request) {
        System.out.println("  [NETBANK] Redirecting to "
            + request.getMetadata("bankCode") + " net banking portal...");
        System.out.println("  [NETBANK] User logged in and approved transaction.");
        System.out.println("  [NETBANK] Bank confirmed debit. Settlement initiated.");
        return PaymentResult.success(
            "Net Banking payment of " + request.getCurrency() + " "
            + request.getAmount() + " via " + request.getMetadata("bankCode"));
    }

    @Override
    public String getProviderName() {
        return "Net Banking";
    }
}

PaymentProcessorFactory.java (Simple Factory)

package com.nbi.payment.factory;

import com.nbi.payment.processor.PaymentProcessor;
import com.nbi.payment.processor.UPIProcessor;
import com.nbi.payment.processor.CreditCardProcessor;
import com.nbi.payment.processor.NetBankingProcessor;

public class PaymentProcessorFactory {

    /**
     * Creates the appropriate payment processor based on the type string.
     * To add a new payment method: create a new Processor class implementing
     * PaymentProcessor, and add one else-if here. No client code changes.
     */
    public static PaymentProcessor createProcessor(String type) {
        if (type == null) {
            throw new IllegalArgumentException("Payment type cannot be null");
        }

        switch (type.toLowerCase()) {
            case "upi":
                return new UPIProcessor();
            case "card":
            case "creditcard":
                return new CreditCardProcessor();
            case "netbanking":
            case "netbank":
                return new NetBankingProcessor();
            default:
                throw new IllegalArgumentException(
                    "Unsupported payment type: " + type
                    + ". Supported: upi, card, netbanking");
        }
    }
}

PaymentService.java

package com.nbi.payment.service;

import com.nbi.payment.factory.PaymentProcessorFactory;
import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.model.ValidationResult;
import com.nbi.payment.processor.PaymentProcessor;

public class PaymentService {

    public PaymentResult processPayment(PaymentRequest request) {
        System.out.println("Processing: " + request);

        // Step 1: Create the correct processor via factory
        PaymentProcessor processor =
            PaymentProcessorFactory.createProcessor(request.getType());
        System.out.println("Provider: " + processor.getProviderName());

        // Step 2: Validate
        ValidationResult validation = processor.validate(request);
        if (!validation.isValid()) {
            System.out.println("Validation failed: " + validation.getErrors());
            return PaymentResult.failure(
                "Validation failed: " + String.join(", ", validation.getErrors()));
        }
        System.out.println("Validation passed.");

        // Step 3: Process
        PaymentResult result = processor.process(request);
        System.out.println("Result: " + result);

        return result;
    }
}

Application.java

package com.nbi.payment;

import com.nbi.payment.model.PaymentRequest;
import com.nbi.payment.model.PaymentResult;
import com.nbi.payment.service.PaymentService;
import java.math.BigDecimal;

public class Application {
    public static void main(String[] args) {
        PaymentService service = new PaymentService();

        // Scenario 1: UPI Payment
        System.out.println("=== Scenario 1: UPI Payment ===");
        PaymentRequest upiRequest = new PaymentRequest("upi", new BigDecimal("5000"))
            .withMetadata("upiId", "rajesh@icici");
        PaymentResult upiResult = service.processPayment(upiRequest);
        System.out.println();

        // Scenario 2: Credit Card Payment
        System.out.println("=== Scenario 2: Credit Card Payment ===");
        PaymentRequest cardRequest = new PaymentRequest("card", new BigDecimal("25000"))
            .withMetadata("cardNumber", "4111222233334444")
            .withMetadata("cvv", "123")
            .withMetadata("expiry", "12/28");
        PaymentResult cardResult = service.processPayment(cardRequest);
        System.out.println();

        // Scenario 3: Net Banking Payment
        System.out.println("=== Scenario 3: Net Banking Payment ===");
        PaymentRequest netbankRequest = new PaymentRequest("netbanking", new BigDecimal("150000"))
            .withMetadata("bankCode", "SBI");
        PaymentResult netbankResult = service.processPayment(netbankRequest);
        System.out.println();

        // Scenario 4: Validation Failure (UPI without UPI ID)
        System.out.println("=== Scenario 4: Validation Failure ===");
        PaymentRequest badRequest = new PaymentRequest("upi", new BigDecimal("5000"));
        PaymentResult badResult = service.processPayment(badRequest);
        System.out.println();

        // Scenario 5: Invalid Payment Type
        System.out.println("=== Scenario 5: Invalid Payment Type ===");
        try {
            PaymentRequest invalidRequest = new PaymentRequest("bitcoin", new BigDecimal("1000"));
            service.processPayment(invalidRequest);
        } catch (IllegalArgumentException e) {
            System.out.println("Error: " + e.getMessage());
        }
        System.out.println();

        // Scenario 6: UPI Amount Limit Exceeded
        System.out.println("=== Scenario 6: UPI Limit Exceeded ===");
        PaymentRequest limitRequest = new PaymentRequest("upi", new BigDecimal("200000"))
            .withMetadata("upiId", "priya@hdfc");
        PaymentResult limitResult = service.processPayment(limitRequest);
    }
}

Expected Output

=== Scenario 1: UPI Payment === Processing: UPI payment of INR 5000 Provider: UPI (NPCI) Validation passed. [UPI] Connecting to NPCI UPI gateway... [UPI] Sending payment request to rajesh@icici [UPI] Waiting for user approval on UPI app... [UPI] Payment approved. Debiting account. Result: SUCCESS | txnId: TXN-A1B2C3D4E5F6 | UPI payment of INR 5000 completed via rajesh@icici === Scenario 2: Credit Card Payment === Processing: CARD payment of INR 25000 Provider: Credit Card (Visa/MasterCard) Validation passed. [CARD] Connecting to card network (Visa/MasterCard)... [CARD] Authorizing card ****-****-****-4444 [CARD] Charge authorized. Processing settlement. Result: SUCCESS | txnId: TXN-G7H8I9J0K1L2 | Credit card payment of INR 25000 charged to ****-****-****-4444 === Scenario 3: Net Banking Payment === Processing: NETBANKING payment of INR 150000 Provider: Net Banking Validation passed. [NETBANK] Redirecting to SBI net banking portal... [NETBANK] User logged in and approved transaction. [NETBANK] Bank confirmed debit. Settlement initiated. Result: SUCCESS | txnId: TXN-M3N4O5P6Q7R8 | Net Banking payment of INR 150000 via SBI === Scenario 4: Validation Failure === Processing: UPI payment of INR 5000 Provider: UPI (NPCI) Validation failed: [UPI ID is required] Result: FAILED | Validation failed: UPI ID is required === Scenario 5: Invalid Payment Type === Error: Unsupported payment type: bitcoin. Supported: upi, card, netbanking === Scenario 6: UPI Limit Exceeded === Processing: UPI payment of INR 200000 Provider: UPI (NPCI) Validation failed: [UPI limit exceeded. Max: 100000] Result: FAILED | Validation failed: UPI limit exceeded. Max: 100000

PaymentProcessorFactoryTest.java

package com.nbi.payment.factory;

import com.nbi.payment.processor.PaymentProcessor;
import com.nbi.payment.processor.UPIProcessor;
import com.nbi.payment.processor.CreditCardProcessor;
import com.nbi.payment.processor.NetBankingProcessor;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class PaymentProcessorFactoryTest {

    @Test
    void createProcessor_returnsUPIProcessor() {
        PaymentProcessor processor = PaymentProcessorFactory.createProcessor("upi");
        assertTrue(processor instanceof UPIProcessor);
    }

    @Test
    void createProcessor_returnsCreditCardProcessor() {
        PaymentProcessor processor = PaymentProcessorFactory.createProcessor("card");
        assertTrue(processor instanceof CreditCardProcessor);
    }

    @Test
    void createProcessor_returnsNetBankingProcessor() {
        PaymentProcessor processor = PaymentProcessorFactory.createProcessor("netbanking");
        assertTrue(processor instanceof NetBankingProcessor);
    }

    @Test
    void createProcessor_isCaseInsensitive() {
        PaymentProcessor p1 = PaymentProcessorFactory.createProcessor("UPI");
        PaymentProcessor p2 = PaymentProcessorFactory.createProcessor("upi");
        assertEquals(p1.getProviderName(), p2.getProviderName());
    }

    @Test
    void createProcessor_throwsForNull() {
        assertThrows(IllegalArgumentException.class,
            () -> PaymentProcessorFactory.createProcessor(null));
    }

    @Test
    void createProcessor_throwsForInvalidType() {
        IllegalArgumentException ex = assertThrows(
            IllegalArgumentException.class,
            () -> PaymentProcessorFactory.createProcessor("bitcoin"));
        assertTrue(ex.getMessage().contains("Unsupported payment type"));
    }

    @Test
    void createProcessor_acceptsAlias() {
        PaymentProcessor p = PaymentProcessorFactory.createProcessor("creditcard");
        assertTrue(p instanceof CreditCardProcessor);
    }

    @Test
    void eachCallCreatesNewInstance() {
        PaymentProcessor p1 = PaymentProcessorFactory.createProcessor("upi");
        PaymentProcessor p2 = PaymentProcessorFactory.createProcessor("upi");
        assertNotSame(p1, p2);
    }
}

Key observations from the project

  1. Loose coupling: PaymentService never imports UPIProcessor, CreditCardProcessor, or NetBankingProcessor. It only knows the PaymentProcessor interface and the factory.
  2. Open for extension: To add a Debit Card processor, create DebitCardProcessor implements PaymentProcessor and add one case to the factory switch. No other file changes.
  3. Each processor validates independently: UPI checks for UPI ID format and amount limit. Credit Card checks for 16-digit number and CVV. Net Banking checks for bank code. The service does not know the rules.
  4. Six scenarios tested: Happy path (UPI, Card, NetBanking), validation failure, invalid type, limit exceeded. All demonstrate factory behavior.
  5. Follows Single Responsibility: Factory creates objects. Processors validate and process. Service orchestrates the flow. Each class has one job.

10. When to Use and When NOT to Use

When to use Factory Pattern

ScenarioExampleWhy Factory
Object creation depends on inputPayment type determines which processor to createClient should not decide; factory centralizes the decision
Multiple implementations of an interfaceJDBC vs Hibernate DAOFactory selects the correct implementation at runtime
Complex object creationCreating a car involves engine, body, interior assemblyFactory hides the complexity from the client
Object creation may changeToday we use MySQL, tomorrow PostgreSQLChange only the factory, not every client
Framework/library designSpring BeanFactory, JDBC DriverManagerUsers get objects without knowing internal creation details

When NOT to use Factory Pattern

ScenarioWhy NOTUse Instead
Single concrete classNo need for abstraction when there is only one implementationDirect instantiation or Singleton
Simple object, no variantsFactory adds unnecessary complexity for trivial objectsnew ClassName()
Object created once at startupFactory is for repeated creation decisions, not one-time setupBuilder or Singleton
Performance-critical hot pathFactory method call adds indirection (negligible in most cases, but matters in tight loops)Direct instantiation

11. Quick Reference Card

Factory Pattern Comparison

AspectSimple FactoryFactory MethodAbstract Factory
GOF Pattern?No (informal pattern)YesYes
Class count1 factory + N products1 abstract + N factories + N products1 abstract + N factory families + N product families
ExtensionModify factory (if/else)Add new factory subclassAdd new factory family
OCP compliantNo (must modify factory)Yes (add subclass)Yes (add family)
Best forSmall projects, few typesMultiple factories, same productsMultiple product families
JDK exampleCalendar.getInstance()Collection.iterator()javax.xml.parsers.DocumentBuilderFactory