Creational Design Pattern
Creational Design Patterns
🎯 Problem
In an e-commerce platform, whenever a customer checks out, we need to integrate with a payment gateway (like Razorpay, Stripe, PhonePe).
👉 But we don’t want to create multiple objects of the payment processor —
-
It may consume unnecessary resources.
-
It could lead to inconsistent states (like two threads trying to pay twice).
-
Payment APIs are usually heavy and costly to initialize.
So, we want only one instance of the payment gateway in the entire application.
Solution → Singleton Design Pattern
The Singleton pattern ensures:
-
Only one instance of a class exists.
-
A global point of access is provided.
🔑 Real-life Mapping
-
Payment Gateway (Singleton) → Only one connection/instance per system.
-
Clients (Cart checkout, Order service, Wallet service) → All use the same gateway instance.
Advantages
-
Saves resources (only one instance).
-
Ensures consistent state (one gateway, one config).
-
Useful for shared resources (DB connection, Logger, Payment Gateway).
🚧 Limitations
-
Hard to test (mocking singleton is tricky).
-
In multithreaded apps, must ensure thread safety (we used
synchronized
).
🔑 Meaning of "One Instance of Payment Gateway Initiated"
-
Instance = an actual object created in memory from a class.
-
When we say only one instance is initiated, it means:
-
Throughout the whole application (no matter how many customers checkout, or how many times you call it),
-
There will be just one object of
PaymentGateway
class created. -
Everyone shares that same object.
-
🛍️ Example in E-commerce
-
Suppose 5 customers checkout at the same time.
-
Without Singleton → Each customer’s checkout might create a new PaymentGateway object → wasteful.
-
With Singleton → All 5 customers reuse the same PaymentGateway instance.
🖼️ Real World Analogy
Think of it like:
-
You are in a mall (your application).
-
The mall has only one cash counter (payment gateway instance).
-
No matter how many customers are there, they all go to the same counter to pay.
If every customer had their own counter, the mall would need unnecessary staff and infrastructure (like multiple gateways in memory).
Only one PaymentGateway object exists in the whole system, and everyone uses it.
🏦 How It Works Internally
-
Think of PaymentGateway as a hub that knows how to talk to different providers (UPI, Card, PhonePe, GPay).
-
Each customer just tells the hub which method to use.
-
The hub routes the request to the correct API of Razorpay/Stripe/PhonePe/etc..
-
Since only one PaymentGateway instance exists, it can handle all requests consistently without re-initializing.
public class PaymentGateway {
// Volatile ensures visibility and prevents instruction reordering
private static volatile PaymentGateway instance;
// Private constructor prevents direct instantiation
private PaymentGateway()
{
System.out.println("PaymentGateway constructor called: " + this);
}
// Double-Checked Locking Singleton
public static PaymentGateway getInstance()
{
if(instance==null)
{
synchronized (PaymentGateway.class)
{
if(instance==null)
{
instance=new PaymentGateway();
}
}
}
return instance;
}
public void processPayment(String method,double amount)
{
System.out.println(Thread.currentThread().getName() +
" Processing " + method + " payment of ₹" + amount +
" using instance: " + this);
}
}
public class FlipkartApp {
public static void main(String[] args) throws InterruptedException {
Runnable task =()->{
PaymentGateway gateway=PaymentGateway.getInstance();
gateway.processPayment("PhonePay",900.00);
};
Thread user1=new Thread(task,"user1");
Thread user2=new Thread(task,"user2");
Thread user3=new Thread(task,"user3");
user1.start();
user2.start();
user3.start();
// Wait for all threads to finish
user1.join();
user2.join();
user3.join();
System.out.println("All users finished payments.");
}
}
👉 Notice:
-
Constructor called only once.
-
All users share the same instance (
PaymentGateway@
3db9389e).
🔑 Why Double Check?
Because of this scenario:
-
Thread-1 enters, sees
instance == null
, enters synchronized block. -
Thread-2 also enters (before Thread-1 finishes creating the object), sees
instance == null
, waits for lock. -
Thread-1 finishes, sets
instance
. -
Thread-2 gets lock, but must check again (the second check).
-
If we don’t check again, it would create a second object.
-
With the second check, it sees
instance != null
and skips creation.
In Simple Words
-
First check: for speed (avoid locking every time).
-
Second check: for safety (ensure only one object created when multiple threads race).
Now suppose:
-
Thread-1 checks →
instance == null
→ goes inside. -
Thread-2 also checks at the same time →
instance == null
→ also goes inside. -
Both threads run
new PaymentGateway()
. -
🎭 Result → two objects created (Singleton broken).
👉 That’s why just one if
check is not enough.
if (instance == null) {
instance = new PaymentGateway();
}
return instance;
}
🔑 Summary
We use volatile
in Singleton because:
-
Prevents instruction reordering during object creation.
-
Ensures visibility of the latest object across threads.
-
Makes Double-Checked Locking safe in Java.
volatile
tells the JVM and CPU:
-
Don’t reorder instructions.
-
Always read/write
instance
directly from main memory (not thread’s cache).
🏭 Factory Design Pattern
🔑 Problem
In your e-commerce platform (Flipkart, Amazon, Myntra):
-
Customers can pay using different methods:
-
UPI
-
Credit/Debit Card
-
NetBanking
-
PhonePe
-
Google Pay
-
👉 If we create objects directly like this:
new UpiPayment();
new CardPayment();
new NetBankingPayment();
Solution → Factory Pattern
Factory Pattern says:
-
Create an interface/abstract class for Payment.
-
Let each payment method implement it.
-
Use a Factory class to create the right object based on input.
-
Client code asks the factory → doesn’t care which class is returned.
public interface Payment {
void pay(double amount);
}
public class UpiPayment implements Payment{
@Override
public void pay(double amount) {
System.out.println("Paid ₹" + amount + " using UPI.");
}
}
public class CardPayment implements Payment{
@Override
public void pay(double amount) {
System.out.println("Paid ₹" + amount + " using Card.");
}
}
public class NetBankingPayment implements Payment{
@Override
public void pay(double amount) {
System.out.println("Paid ₹" + amount + " using NetBanking.");
}
}
public class PaymentFactory {
public static Payment getPaymentMethod(String type)
{
switch(type.toLowerCase())
{
case "upi":
return new UpiPayment();
case "card":
return new CardPayment();
case "netbanking":
return new NetBankingPayment();
default:
throw new IllegalArgumentException("Invalid payment method: " + type);
}
}
}
public class FlipkartCheckout {
public static void main(String[] args) {
// Payment type comes dynamically (e.g., from user input / DB / config)
Payment p1 = PaymentFactory.getPaymentMethod("upi");
p1.pay(1200);
Payment p2 = PaymentFactory.getPaymentMethod("card");
p2.pay(3000);
Payment p3 = PaymentFactory.getPaymentMethod("phonepe");
p3.pay(499);
}
}
👉 Here, if tomorrow you add CryptoPayment:
-
You only change
PaymentFactory
(addcase "crypto": return new CryptoPayment();
). -
Checkout/main method stays untouched.
main .
In below diagram everytime we have to change main class when you add new payment way which is not good practise and it violates the open close principle.So we use factory to create a object of type at central level and pass this value dynamically.
Payment upi = new UpiPayment();
upi.pay(1200);
Payment card = new CardPayment();
card.pay(3000);
// Tomorrow: new mode added
Payment phonepe = new PhonePePayment(); // 🚨 client code changed
phonepe.pay(499);
🔹 Problem Without Factory
-
In a simple design, your main class creates objects directly:
Payment p2 = new CardPayment();
Payment p = new UpiPayment();
PhonePePayment
is added, you must edit the main class:❌ This is bad practice because:
-
Violates Open/Closed Principle → main class is not closed for modification.
-
Main class becomes tightly coupled to all payment classes.
-
Harder to maintain when more payment types are added.
🔹 Solution With Factory
-
Factory centralizes object creation.
-
Main class doesn’t
new
objects directly. Instead, it asks the factory:
🏗️ Builder Design Pattern
🔑 Problem
In e-commerce apps (Amazon, Flipkart, Myntra), suppose you are building a Product
object.
A Product
can have many optional fields:
-
id
(mandatory) -
name
(mandatory) -
price
(mandatory) -
description
(optional) -
discount
(optional) -
rating
(optional) -
seller
(optional)
👉 If you use constructor overloading:
new Product(1, "Shoes", 2999);
new Product(2, "Shoes", 2999, "Running shoes", 10);
new Product(3, "Shoes", 2999, "Running shoes", 10, 4.5, "Nike");
Solution → Builder Pattern
-
Builder pattern helps construct complex objects step by step.
-
You can set only the fields you want.
-
Final object is created using a
build()
method.
import java.lang.constant.Constable;
public class Product {
private int id;
private String name;
private double price;
// optional fields
private String description;
private double discount;
private double rating;
private String seller;
Product(ProductBuilder builder)
{
this.id = builder.id;
this.name = builder.name;
this.price = builder.price;
this.description = builder.description;
this.discount = builder.discount;
this.rating =builder. rating;
this.seller = builder.seller;
}
@Override
public String toString() {
return "Product [id=" + id +
", name=" + name +
", price=" + price +
", description=" + description +
", discount=" + discount +
", rating=" + rating +
", seller=" + seller + "]";
}
}
public class ProductBuilder {
int id;
String name;
double price;
// optional fields
String description;
double discount;
double rating;
String seller;
public ProductBuilder(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Setter methods return the Builder itself (method chaining)
public ProductBuilder description(String description)
{
this.description=description;
return this;
}
public ProductBuilder discount(double discount)
{
this.discount=discount;
return this;
}
public ProductBuilder rating(double rating) {
this.rating = rating;
return this;
}
public ProductBuilder seller(String seller) {
this.seller = seller;
return this;
}
public Product build()
{
return new Product(this);
}
}
public class TestBuilder {
public static void main(String[] args) {
Product nike= new ProductBuilder(101, "Nike Shoe", 2999.0)
.description("Lightweight running shoe")
.discount(10.0)
.rating(4.5)
.seller("Nike Store")
.build();
Product adidas=new ProductBuilder(102, "Adidas Shoe", 1999.0)
.build();// 👉 no optional fields
Product puma = new ProductBuilder(103, "Puma Shoe", 2499.0)
.discount(15.0)
.build(); // 👉 only some optional fields
System.out.println(nike);
// Build Product objects dynamically // real time in flipkart
// discuss in FlipkartSearch class
}
}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class FlipkartSearch {
public static void main(String[] args) {
List<Map<String,Object>> dbResult=new ArrayList<>();
Map<String,Object> nike= new HashMap<>();
nike.put("id", 101);
nike.put("name", "Nike Shoe");
nike.put("price", 2999.0);
nike.put("description", "Lightweight running shoe");
nike.put("discount", 10.0);
nike.put("rating", 4.5);
nike.put("seller", "Nike Store");
dbResult.add(nike);
Map<String, Object> adidas = new HashMap<>();
adidas.put("id", 102);
adidas.put("name", "Adidas Shoe");
adidas.put("price", 1999.0);
dbResult.add(adidas); // no optional fields
Map<String, Object> puma = new HashMap<>();
puma.put("id", 103);
puma.put("name", "Puma Shoe");
puma.put("price", 2499.0);
puma.put("discount", 15.0);
puma.put("rating", 4.2);
dbResult.add(puma);
// Build Product objects dynamically
List<Product>products= new ArrayList<>();
for(Map<String,Object> row:dbResult)
{
ProductBuilder builder=new ProductBuilder(
(int) row.get("id"),
(String) row.get("name"),
(double)row.get("price")
);
if (row.containsKey("description"))
builder.description((String) row.get("description"));
if (row.containsKey("discount"))
builder.discount((double) row.get("discount"));
if (row.containsKey("rating"))
builder.rating((double) row.get("rating"));
if (row.containsKey("seller"))
builder.seller((String) row.get("seller"));
products.add(builder.build());
}
// Print all products
for (Product p : products) {
System.out.println(p);
}
}
}
In Flipkart (or Amazon, Myntra, etc.), product objects are created dynamically at runtime:
-
Backend gets product data from DB or cache.
-
Some rows have optional fields (discount, rating, seller).
-
Some rows don’t.
-
-
For each row, a Builder is used:
-
Mandatory fields → always set.
-
Optional fields → set only if they exist (
if (row.containsKey(...))
).
-
-
When
.build()
is called → one Product object is created with whatever fields were provided. -
If discount/rating were not provided → they remain
null
or default. -
But the object is still valid and returned to frontend.
Comments
Post a Comment