Structural Design Patterns
🚀 What are Structural Design Patterns?
Structural patterns are about how classes and objects are combined to form larger structures.
Think of them as blueprints for connecting components together efficiently.
They help when:
-
You want to reuse code without rewriting it.
-
You want to adapt incompatible interfaces.
-
You want to simplify complex structures.
📑 List of Structural Design Patterns
There are 7 key patterns:
-
Adapter → Convert one interface into another the client expects.
-
Bridge → Decouple abstraction from implementation.
-
Composite → Tree structures (part-whole hierarchy).
-
Decorator → Add responsibilities to objects dynamically.
-
Facade → Provide a simplified interface to a complex system.
-
Flyweight → Reuse objects to save memory.
-
Proxy → Control access to an object.
⚡ How we’ll learn them
To make it easy, let’s take real-world examples + Java code:
-
Adapter → Charger plug adapter (110V to 220V).
-
Bridge → Remote control works with different TVs.
-
Composite → Folders & files in a file system.
-
Decorator → Adding toppings to a pizza.
-
Facade → Home theater system (one button → many subsystems).
-
Flyweight → Characters in a text editor.
-
Proxy → ATM machine as proxy for bank.
Problem:
An e-commerce platform supports multiple payment providers (PayPal, Stripe, Razorpay, PhonePe, GPay, Jupiter), each with a different interface. We need a standardized way to integrate them.
Solution:Use the Adapter Pattern to convert the interface of a third-party payment gateway into a common interface expected by the application.
Benefits:
✔ Integrates incompatible interfaces without modifying code.✔ Adds new payment providers without changing the existing system.
.
🧩 What is Adapter Pattern?
👉 The Adapter Pattern is a structural design pattern.
It allows two incompatible interfaces to work together without changing their code.
Think of it as a translator 🗣 between two people speaking different languages.
🔎 Why do we need it (Use)?
-
You have some existing code (e.g., your e-commerce system expects a
PaymentProcessor
). -
You want to use 3rd-party libraries (PayPal, Stripe, Razorpay, etc.), but their APIs (methods) are different.
-
Instead of rewriting everything, you create an Adapter that makes those APIs look the same to your system.
👉 This way:
-
Your system stays clean.
-
You can add new payment providers easily without touching the client code.
📌 Example in Plain Words
Imagine:
-
You know only Hindi.
-
PayPal speaks English, Stripe speaks French, Razorpay speaks Japanese.
Without adapter: You must learn English, French, and Japanese. (Too much work).
With adapter (translator):
-
You always speak Hindi.
-
Translator (adapter) translates Hindi → English/French/Japanese.
-
You don’t care what language they speak.
👉 You only remember Hindi, not every other language.
Same in code → client only remembers pay(amount)
.
🛒 E-Commerce Payments – Before & After Adapter Pattern
❌ Before Adapter (messy integration)
Problem:
-
Each provider has its own unique method name.
-
Client code has to know every API → tightly coupled, not extensible.
public class BeforeAdapterDemo {
public static void main(String[] args) {
// Direct integration with third-party APIs
// PayPal
PayPalGateway paypal = new PayPalGateway();
paypal.makePayment(250.0);
// Stripe
StripeGateway stripe = new StripeGateway();
stripe.doPayment(499.0);
// Razorpay
RazorpayGateway razorpay = new RazorpayGateway();
razorpay.processRazorpayPayment(999.0);
// If tomorrow we add GPay → client must learn its new API 😓
}
}
❌ Problems:
-
Client must remember different method names:
-
makePayment()
for PayPal -
doPayment()
for Stripe -
processRazorpayPayment()
for Razorpay
-
-
Adding a new provider → modify client code everywhere.
-
Tight coupling with vendors.
public class PayPalGateway {
public void makePayment(double money)
{
System.out.println("Payment done through paypal Gateway");
}
}
package org.example.design.StructuralDesign.Adapter;
public class StripeGateway {
public void doPayment(double money)
{
System.out.println("payment done through Stripe Gateway");
}
}
public class StripeGateway {
public void doPayment(double money)
{
System.out.println("payment done through Stripe Gateway");
}
}
package org.example.design.StructuralDesign.Adapter;
public class RazorpayGateway {
public void processRazorpayPayment(double amountInINR) {
System.out.println("payment done through Razorpay Gateway");
}
}
public class RazorpayGateway {
public void processRazorpayPayment(double amountInINR) {
System.out.println("payment done through Razorpay Gateway");
}
}
Main Class -problem Tight Coupling (api name-Method name)
public class BeforeAdapterDemo {
public static void main(String[] args) {
// Direct integration with third-party APIs
// PayPal
PayPalGateway paypal = new PayPalGateway();
paypal.makePayment(250.0);
// Stripe
StripeGateway stripe = new StripeGateway();
stripe.doPayment(499.0);
// Razorpay
RazorpayGateway razorpay = new RazorpayGateway();
razorpay.processRazorpayPayment(999.0);
// If tomorrow we add GPay → client must learn its new API 😓
}
}
After Adapter (clean & unified)
Solution:
-
Define a common interface (
PaymentProcessor
). -
Write adapters for each provider.
-
Client only depends on
PaymentProcessor
.
With Adapter (solution)
-
You create one common interface:
PaymentProcessor { pay(amount) }
-
For each provider, you write a small Adapter class that translates
.pay(amount)
into the provider’s method.
👉 Now the client always calls .pay(amount)
, no matter which provider is used.
public class PayPalAdapter implements PaymentProcessor{
PayPalGateway payPalGateway;
public PayPalAdapter(PayPalGateway payPalGateway) {
this.payPalGateway = payPalGateway;
}
@Override
public void pay(double amount) {
payPalGateway.makePayment(amount);
}
}
public class AfterAdapterTest {
public static void main(String[] args) {
// Client now works with a single interface PaymentProcessor
PaymentProcessor paypal=new PayPalAdapter(new PayPalGateway());
paypal.pay(200.00);
PaymentProcessor stripeProcessor = new StripeAdapter(new StripeGateway());
stripeProcessor.pay(499.0);
PaymentProcessor razorpayProcessor = new RazorpayAdapter(new RazorpayGateway());
razorpayProcessor.pay(999.0);
// Tomorrow if GPay or PhonePe comes → just add new adapter
}
}
🚀 Benefits (Now Super Clear)
-
Client only calls
.pay()
— never cares about vendor APIs (upiPay
,makePayment
, etc). -
One unified flow — you don’t keep creating multiple adapter objects manually.
-
System picks correct adapter at runtime.
-
-
Easily extensible — Add PhonePe tomorrow → just write
PhonePeAdapter
+ update factory. -
Client code stays untouched.
public static PaymentProcessor getPaymentProcessor(String type) {
return switch (type) {
case "PayPal" -> new PayPalAdapter(new PayPalGateway());
case "Stripe" -> new StripeAdapter(new StripeGateway());
case "GPay" -> new GPayAdapter(new GPayGateway());
default -> throw new IllegalArgumentException("Unknown payment type");
};
}
}
public static void main(String[] args) {
String selected = "GPay"; // assume user selected GPay from UI
PaymentProcessor processor = PaymentFactory.getPaymentProcessor(selected);
processor.pay(500); // client still calls only .pay()
}
}
🎁 Decorator Pattern
🔎 Problem
-
An e-commerce platform sells products.
-
Customers want optional features:
-
Gift wrap
-
Extended warranty
-
Personalization (engraving, custom message, etc.)
-
-
These should be added dynamically, without changing the core
Product
class.
Solution (Decorator Pattern)
-
Define a common interface for product.
-
Create a base product (e.g., Laptop, Phone).
-
Wrap the product with decorators (GiftWrapDecorator, WarrantyDecorator, PersonalizationDecorator).
-
Each decorator adds extra cost or behavior.
❌ Before (Without Decorator)
Problem:
-
Every time we add a new feature (Gift Wrap, Warranty, Personalization), we would need a new subclass.
-
Example:
-
LaptopWithGiftWrap
-
LaptopWithWarranty
-
LaptopWithGiftWrapAndWarranty
-
LaptopWithGiftWrapAndPersonalization
-
-
This quickly leads to class explosion 💥.
public class Laptop {
public String getDescription()
{
return "Laptop";
}
public double getCost()
{
return 500.00;
}
}
public class LaptopWithGiftWrap extends Laptop{
@Override
public String getDescription() {
return super.getDescription() + ", Gift Wrapped";
}
@Override
public double getCost() {
return super.getCost() + 200.0;
}
}
public class LaptopWithWarranty extends Laptop{
@Override
public String getDescription() {
return super.getDescription()+ ", Extended Warranty";
}
@Override
public double getCost() {
return super.getCost()+1500;
}
}
// If customer wants GiftWrap + Warranty + Personalization → new class needed ❌
❌ Problems:
-
Too many subclasses.
-
Hard to maintain.
-
Any new feature = create more subclasses.
After (With Decorator)
Solution:
-
We keep one base product class (
Laptop
). -
Each feature (GiftWrap, Warranty, Personalization) is a decorator.
-
We can combine decorators dynamically at runtime → no explosion of subclasses.
🚀 Benefits of After (Decorator)
✔ Add/remove features dynamically (no new classes).
✔ Core Laptop
class is unchanged.
✔ Avoids class explosion.
✔ Flexible: Any combination of features at runtime.
📌 Interview Summary:
-
Before: Inheritance → subclass explosion.
-
After: Composition with Decorator → flexible, scalable.
Real-World Style: Dynamic Decorator Usage
Imagine a customer is shopping on an e-commerce site:
-
Selects Laptop as the base product.
-
Then ticks checkboxes: Gift Wrap, Warranty, ❌ Personalization.
We can build decorators dynamically based on user input.
public interface Product {
String getDescription();
double getCost();
}
public class Laptop implements Product {
public String getDescription()
{
return "Laptop";
}
public double getCost()
{
return 500.00;
}
}
abstract class ProductDecorator implements Product{
protected Product product;
public ProductDecorator(Product product) {
this.product = product;
}
@Override
public String getDescription() {
return product.getDescription();
}
@Override
public double getCost() {
return product.getCost();
}
}
public class GiftWrapDecorator extends ProductDecorator{
public GiftWrapDecorator(Product product) {
super(product);
}
@Override
public String getDescription() {
return super.getDescription()+ ", Gift Wrapped";
}
@Override
public double getCost() {
return super.getCost() + 200.0;
}
}
public class WarrantyDecorator extends ProductDecorator{
public WarrantyDecorator(Product product) {
super(product);
}
@Override
public String getDescription() {
return super.getDescription()+ ", Warranty Extended";
}
@Override
public double getCost() {
return super.getCost() + 1200.0;
}
}
import java.util.Scanner;
public class EcommerceClientTest {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Product product= new Laptop();
// Dynamic add-ons
System.out.println("Do you want Gift Wrap? (yes/no)");
if(sc.next().equalsIgnoreCase("yes"))
{
product=new GiftWrapDecorator(product);
}
System.out.println("Do you want Extended Warranty? (yes/no)");
if (sc.next().equalsIgnoreCase("yes")) {
product = new WarrantyDecorator(product);
}
// Final product
System.out.println("\nFinal Order: " + product.getDescription());
System.out.println("Total Cost: ₹" + product.getCost());
}
}
yes
Do you want Extended Warranty? (yes/no)
yes
Do you want Personalization? (yes/no)
no
Final Order: Laptop, Gift Wrapped, Extended Warranty
Total Cost: ₹1900.0
no
Do you want Extended Warranty? (yes/no)
no
Final Order: Laptop
Total Cost: ₹500.0
yes
Do you want Extended Warranty? (yes/no)
no
Final Order: Laptop, Gift Wrapped
Total Cost: ₹700.0
Process finished with exit code 0
🚀 Why this is Real-World?
-
Customer choices decide which decorators are applied.
-
You don’t pre-define all combinations → system builds it dynamically.
-
Very similar to Amazon/Flipkart “Add-on features”.
🎭 Facade Design Pattern
📌 Problem
In large systems (like an e-commerce checkout process), a client may need to interact with multiple subsystems:
-
InventoryService
-
PaymentService
-
ShippingService
-
NotificationService
👉 If the client calls each of these directly:
-
The code becomes complex.
-
Changes in subsystems ripple into the client.
📌 Solution
Use a Facade → a single class that provides a simplified interface to the client.
-
The facade internally calls all subsystems in the right sequence.
-
The client just uses one unified method like
placeOrder()
.
public class ClientBeforeFacade {
public static void main(String[] args) {
// Client talking to multiple services directly ❌
InventoryService inventory = new InventoryService();
PaymentService payment = new PaymentService();
ShippingService shipping = new ShippingService();
NotificationService notification = new NotificationService();
// Too many steps exposed to client
if (inventory.checkStock("Laptop")) {
if (payment.makePayment("Laptop", 50000)) {
shipping.shipProduct("Laptop");
notification.sendConfirmation("Laptop");
}
}
}
}
public class InventoryService {
public boolean checkStock(String Product) {
System.out.println("Checking stock for " + Product);
return true;
}
}
public class PaymentService {
public boolean makePayment(String Product, double amount) {
System.out.println("Processing payment of ₹" + amount + " for " + Product);
return true;
}
}
public class ShippingService {
public void shipProduct(String Product) {
System.out.println("Shipping " + Product);
}
}
public class NotificationService {
public void sendConfirmation(String Product) {
System.out.println("Confirmation sent for " + Product);
}
}
public class FacadeClientAfterfacadeTest {
public static void main(String[] args) {
OrderFacade orderFacade=new OrderFacade();
orderFacade.placeOrder("Laptop",50000);
}
}
Processing payment of ₹50000.0 for Laptop
Shipping Laptop
Confirmation sent for Laptop
Order placed successfully
📌 In real-world e-commerce, OrderFacade
could be a service layer class that orchestrates:
-
Inventory, Payment, Shipping, Notification services.
-
Client (like a Controller or API) just calls
orderFacade.placeOrder()
.
🎯 Main Benefit of Facade Pattern
👉 It hides complexity and provides one simple entry point for the client.
Instead of the client knowing:
-
What order to call services in
-
How they depend on each other
-
Which validations are required
…the Facade does all the orchestration.
The client just calls one method (like placeOrder()
).
❌ Without Facade (Bad Way)
Yes, in the non-facade world the client (UI / API caller) would have to manually:
-
Call
InventoryService.checkStock()
-
Then call
PaymentService.makePayment()
-
Then call
ShippingService.shipProduct()
-
Then call
NotificationService.sendConfirmation()
👉 That means the client is responsible for knowing the sequence and business flow.
If tomorrow you change the flow (e.g., payment before stock check), you’d have to change every client (mobile app, web app, admin panel). Painful ❌.
With Facade
The client just clicks “Place Order” (or calls orderFacade.placeOrder()
).
The facade handles everything internally:
-
Checks stock
-
Processes payment
-
Ships product
-
Sends notification
👉 So the client doesn’t need to click step by step or know the order of operations.
It only does one action → placeOrder()
.
🛒 Example in real life (Amazon):
-
You click “Place Order” once.
-
Amazon internally:
-
Reserves stock
-
Charges your card
-
Books courier
-
Sends SMS/email confirmation
-
You don’t click “Check Stock” → “Pay” → “Ship” → “Notify” separately.
That’s exactly what the Facade Pattern models.
🎭 Proxy Design Pattern
📌 Problem
Sometimes, we don’t want clients to access the real object directly because:
-
It may be expensive to create (e.g., huge object, remote connection).
-
We may want to control access (e.g., authentication, logging, caching).
-
We may want lazy loading (load only when needed).
👉 Example:
In a video streaming platform (Netflix/YouTube), a client should not directly load a heavy video file immediately. Instead, use a Proxy to control when/how the video is loaded.
🚀 Benefits of Proxy
✔ Lazy loading → Load heavy objects only when needed.
✔ Access control → Add security checks (like login required).
✔ Logging / caching → Track usage or cache results.
✔ Same interface → Client doesn’t know whether it’s using real object or proxy.
Comments
Post a Comment