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:

  1. Only one instance of a class exists.

  2. A global point of access is provided.


🔑 Real-life Mapping



 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).


PaymentGateway g1 = PaymentGateway.getInstance();
PaymentGateway g2 = PaymentGateway.getInstance();

System.out.println(g1 == g2); // true


➡️ Meaning: both g1 and g2 point to the same single object in memory.

 So “one instance of payment gateway initiated” =

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.


package org.example.design.CreationalDesignPattern.Singleton;

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);
}

}

package org.example.design.CreationalDesignPattern.Singleton;

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.");
}
}

PaymentGateway constructor called: org.example.design.CreationalDesignPattern.Singleton.PaymentGateway@3db9389e
user2 Processing PhonePay payment of ₹900.0 using instance: org.example.design.CreationalDesignPattern.Singleton.PaymentGateway@3db9389e
user3 Processing PhonePay payment of ₹900.0 using instance: org.example.design.CreationalDesignPattern.Singleton.PaymentGateway@3db9389e
user1 Processing PhonePay payment of ₹900.0 using instance: org.example.design.CreationalDesignPattern.Singleton.PaymentGateway@3db9389e
All users finished payments.


👉 Notice:

  • Constructor called only once.

  • All users share the same instance (PaymentGateway@3db9389e).



🔑 Why Double Check?

Because of this scenario:

  1. Thread-1 enters, sees instance == null, enters synchronized block.

  2. Thread-2 also enters (before Thread-1 finishes creating the object), sees instance == null, waits for lock.

  3. Thread-1 finishes, sets instance.

  4. 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).


🔎 Case 1: Without Synchronization



if (instance == null) {
    instance = new PaymentGateway();  // object creation
}
return instance;


Now suppose:

  • Thread-1 checks → instance == null → goes inside.

  • Thread-2 also checks at the same timeinstance == 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.


🔎 Case 2: With Synchronization (but no double check)


public static synchronized PaymentGateway getInstance() {
if (instance == null) {
instance = new PaymentGateway();
}
return instance;
}



Now only one thread at a time can enter.
 This is safe (only one object ever created).
❌ But every single call to getInstance() will lock → even after the object is already created.
Millions of payments → unnecessary performance cost.



🔑 Summary

We use volatile in Singleton because:

  1. Prevents instruction reordering during object creation.

  2. Ensures visibility of the latest object across threads.

  3. 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();

Our code becomes tightly coupled to specific classes.

Every time a new payment method comes, we must change client code → violates OCP (Open/Closed Principle).



 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.


🖥️ Implementation in Java

package org.example.design.Creational.FactoryPattern;

public interface Payment {
void pay(double amount);
}


package org.example.design.Creational.FactoryPattern;

public class UpiPayment implements Payment{
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using UPI.");
}
}

package org.example.design.Creational.FactoryPattern;

public class CardPayment implements Payment{
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using Card.");
}
}

package org.example.design.Creational.FactoryPattern;

public class NetBankingPayment implements Payment{
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using NetBanking.");
}
}


package org.example.design.Creational.FactoryPattern;

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);

}
}
}


package org.example.design.Creational.FactoryPattern;

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 (add case "crypto": return new CryptoPayment();).

  • Checkout/main method stays untouched.


so that the payment mode doesn’t even need to be hardcoded in 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.

// Existing modes
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();



If tomorrow a new payment type PhonePePayment is added, you must edit the main class:

Payment p3 = new PhonePePayment();


❌ This is bad practice because:

  1. Violates Open/Closed Principle → main class is not closed for modification.

  2. Main class becomes tightly coupled to all payment classes.

  3. 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");



❌ Hard to read which parameter is which.

❌ Too many constructors → telescoping constructors problem.

❌ Not flexible (if more fields are added, all constructors break).




 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.



🖥️ Implementation in Java


package org.example.design.create.builder;

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 + "]";
}
}


package org.example.design.create.builder;

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);
}


}



package org.example.design.create.builder;

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




}
}


Real Time Example - Main class


package org.example.design.create.builder;

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:

  1. Backend gets product data from DB or cache.

    • Some rows have optional fields (discount, rating, seller).

    • Some rows don’t.

  2. For each row, a Builder is used:

    • Mandatory fields → always set.

    • Optional fields → set only if they exist (if (row.containsKey(...))).

  3. 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

Popular posts from this blog

Two Sum II - Input Array Is Sorted

Comparable Vs. Comparator in Java

Increasing Triplet Subsequence