Factory and Factory Method Pattern
1. What is Factory Pattern?
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
- Factory Pattern internally contains one static factory method or non-static method and provides abstraction on the object creation process.
- 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 Method | Static / Non-Static | Returns |
|---|---|---|
Thread.currentThread() | Static | Returns its own class object (Thread) |
Class.forName("java.util.Date") | Static | Returns its own class object (Class) |
Calendar.getInstance() | Static | Returns related class object (GregorianCalendar subclass) |
Properties.getProperties() | Static | Returns related class object (between System class and Properties class) |
DriverManager.getConnection(url, user, pwd) | Static | Returns related class object (driver-specific Connection) |
con.createStatement() | Non-Static | Returns related class object (called on Connection object) |
st.executeQuery() | Non-Static | Returns related class object (ResultSet) |
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
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();
}
}
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
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.
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?
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
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.
| Aspect | Factory Method | Factory Pattern |
|---|---|---|
| Scope | A single method that creates and returns an object | A class structure that uses a factory method to create related objects |
| Return type | Can return any Java class object | Always returns one of several related class objects |
| Examples | Properties.getProperty() is a factory method but not a factory pattern | DriverManager.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:
| Pattern | Description | Structure | When to Use |
|---|---|---|---|
| Simple Factory | One factory class, one method, creates objects via if/else or switch | 1 factory class, many products | Simple object creation, few product types |
| Factory Method | Abstract factory class defines the creation method. Subclasses decide which class to instantiate | 1 abstract factory, many concrete factory subclasses | Multiple factories for same product family, each with its own logic |
| Abstract Factory | Factory of factories. Provides interface for creating families of related objects | Factory that returns other factories | Multiple product families (e.g., JDBC vs Hibernate) |
"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
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
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();
}
}
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
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
// 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
| Aspect | Simple Factory | Factory Method | Abstract Factory |
|---|---|---|---|
| Structure | One factory class creates all objects | Abstract factory, concrete factory subclasses decide which class to instantiate | Factory of factories, creates families of related objects |
| Approach | if/else or switch in one method | Inheritance-based (subclass overrides factory method) | Composition-based (factory returns other factories) |
| Products | One product type, multiple variants | One product type, factory-specific creation | Multiple product types, family-based |
| Flexibility | Low (adding types requires changing factory) | Medium (new factory subclass for new logic) | High (new family = new factory, no changes to existing) |
| Use when | Few product types, simple creation | Each factory needs different creation logic | Multiple product families (JDBC vs Hibernate, UI themes) |
9. Full-Length Industry Project: Payment Gateway System
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)
Low-Level Design (LLD)
Project Structure
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
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
- Loose coupling:
PaymentServicenever importsUPIProcessor,CreditCardProcessor, orNetBankingProcessor. It only knows thePaymentProcessorinterface and the factory. - Open for extension: To add a Debit Card processor, create
DebitCardProcessor implements PaymentProcessorand add one case to the factory switch. No other file changes. - 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.
- Six scenarios tested: Happy path (UPI, Card, NetBanking), validation failure, invalid type, limit exceeded. All demonstrate factory behavior.
- 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
| Scenario | Example | Why Factory |
|---|---|---|
| Object creation depends on input | Payment type determines which processor to create | Client should not decide; factory centralizes the decision |
| Multiple implementations of an interface | JDBC vs Hibernate DAO | Factory selects the correct implementation at runtime |
| Complex object creation | Creating a car involves engine, body, interior assembly | Factory hides the complexity from the client |
| Object creation may change | Today we use MySQL, tomorrow PostgreSQL | Change only the factory, not every client |
| Framework/library design | Spring BeanFactory, JDBC DriverManager | Users get objects without knowing internal creation details |
When NOT to use Factory Pattern
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Single concrete class | No need for abstraction when there is only one implementation | Direct instantiation or Singleton |
| Simple object, no variants | Factory adds unnecessary complexity for trivial objects | new ClassName() |
| Object created once at startup | Factory is for repeated creation decisions, not one-time setup | Builder or Singleton |
| Performance-critical hot path | Factory method call adds indirection (negligible in most cases, but matters in tight loops) | Direct instantiation |
11. Quick Reference Card
Factory Pattern Comparison
| Aspect | Simple Factory | Factory Method | Abstract Factory |
|---|---|---|---|
| GOF Pattern? | No (informal pattern) | Yes | Yes |
| Class count | 1 factory + N products | 1 abstract + N factories + N products | 1 abstract + N factory families + N product families |
| Extension | Modify factory (if/else) | Add new factory subclass | Add new factory family |
| OCP compliant | No (must modify factory) | Yes (add subclass) | Yes (add family) |
| Best for | Small projects, few types | Multiple factories, same products | Multiple product families |
| JDK example | Calendar.getInstance() | Collection.iterator() | javax.xml.parsers.DocumentBuilderFactory |
Factory and Factory Method Design Pattern in Java
1. What is Factory Pattern?
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
- Factory Pattern internally contains one static factory method or non-static method and provides abstraction on the object creation process.
- 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 Method | Static / Non-Static | Returns |
|---|---|---|
Thread.currentThread() | Static | Returns its own class object (Thread) |
Class.forName("java.util.Date") | Static | Returns its own class object (Class) |
Calendar.getInstance() | Static | Returns related class object (GregorianCalendar subclass) |
Properties.getProperties() | Static | Returns related class object (between System class and Properties class) |
DriverManager.getConnection(url, user, pwd) | Static | Returns related class object (driver-specific Connection) |
con.createStatement() | Non-Static | Returns related class object (called on Connection object) |
st.executeQuery() | Non-Static | Returns related class object (ResultSet) |
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
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();
}
}
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
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.
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?
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
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.
| Aspect | Factory Method | Factory Pattern |
|---|---|---|
| Scope | A single method that creates and returns an object | A class structure that uses a factory method to create related objects |
| Return type | Can return any Java class object | Always returns one of several related class objects |
| Examples | Properties.getProperty() is a factory method but not a factory pattern | DriverManager.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:
| Pattern | Description | Structure | When to Use |
|---|---|---|---|
| Simple Factory | One factory class, one method, creates objects via if/else or switch | 1 factory class, many products | Simple object creation, few product types |
| Factory Method | Abstract factory class defines the creation method. Subclasses decide which class to instantiate | 1 abstract factory, many concrete factory subclasses | Multiple factories for same product family, each with its own logic |
| Abstract Factory | Factory of factories. Provides interface for creating families of related objects | Factory that returns other factories | Multiple product families (e.g., JDBC vs Hibernate) |
"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
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
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();
}
}
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
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
// 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
| Aspect | Simple Factory | Factory Method | Abstract Factory |
|---|---|---|---|
| Structure | One factory class creates all objects | Abstract factory, concrete factory subclasses decide which class to instantiate | Factory of factories, creates families of related objects |
| Approach | if/else or switch in one method | Inheritance-based (subclass overrides factory method) | Composition-based (factory returns other factories) |
| Products | One product type, multiple variants | One product type, factory-specific creation | Multiple product types, family-based |
| Flexibility | Low (adding types requires changing factory) | Medium (new factory subclass for new logic) | High (new family = new factory, no changes to existing) |
| Use when | Few product types, simple creation | Each factory needs different creation logic | Multiple product families (JDBC vs Hibernate, UI themes) |
9. Full-Length Industry Project: Payment Gateway System
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)
Low-Level Design (LLD)
Project Structure
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
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
- Loose coupling:
PaymentServicenever importsUPIProcessor,CreditCardProcessor, orNetBankingProcessor. It only knows thePaymentProcessorinterface and the factory. - Open for extension: To add a Debit Card processor, create
DebitCardProcessor implements PaymentProcessorand add one case to the factory switch. No other file changes. - 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.
- Six scenarios tested: Happy path (UPI, Card, NetBanking), validation failure, invalid type, limit exceeded. All demonstrate factory behavior.
- 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
| Scenario | Example | Why Factory |
|---|---|---|
| Object creation depends on input | Payment type determines which processor to create | Client should not decide; factory centralizes the decision |
| Multiple implementations of an interface | JDBC vs Hibernate DAO | Factory selects the correct implementation at runtime |
| Complex object creation | Creating a car involves engine, body, interior assembly | Factory hides the complexity from the client |
| Object creation may change | Today we use MySQL, tomorrow PostgreSQL | Change only the factory, not every client |
| Framework/library design | Spring BeanFactory, JDBC DriverManager | Users get objects without knowing internal creation details |
When NOT to use Factory Pattern
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Single concrete class | No need for abstraction when there is only one implementation | Direct instantiation or Singleton |
| Simple object, no variants | Factory adds unnecessary complexity for trivial objects | new ClassName() |
| Object created once at startup | Factory is for repeated creation decisions, not one-time setup | Builder or Singleton |
| Performance-critical hot path | Factory method call adds indirection (negligible in most cases, but matters in tight loops) | Direct instantiation |
11. Quick Reference Card
Factory Pattern Comparison
| Aspect | Simple Factory | Factory Method | Abstract Factory |
|---|---|---|---|
| GOF Pattern? | No (informal pattern) | Yes | Yes |
| Class count | 1 factory + N products | 1 abstract + N factories + N products | 1 abstract + N factory families + N product families |
| Extension | Modify factory (if/else) | Add new factory subclass | Add new factory family |
| OCP compliant | No (must modify factory) | Yes (add subclass) | Yes (add family) |
| Best for | Small projects, few types | Multiple factories, same products | Multiple product families |
| JDK example | Calendar.getInstance() | Collection.iterator() | javax.xml.parsers.DocumentBuilderFactory |