Introduction to Design Patterns

As software grows, code can quickly become:

  • Difficult to read
  • Hard to maintain
  • Rigid and fragile
  • Difficult to extend

Design patterns provide proven solutions to common software design problems.

They are not libraries. They are not frameworks. They are structured approaches to writing better code.


1️⃣ Singleton Pattern

✅ Purpose

Ensure a class has only one instance and provide a global access point to it.

✅ When to Use

  • Logger
  • Configuration manager
  • Database connection
  • Caching system

✅ Basic Implementation

public class Logger {

    private static Logger instance;

    private Logger() {
        // private constructor prevents external instantiation
    }

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

    public void log(String message) {
        System.out.println(message);
    }
}

✅ Usage

Logger logger = Logger.getInstance();
logger.log("Application started");

⚠ Important Note

Singleton can become an anti-pattern if:

  • Overused
  • It introduces global state
  • It makes testing difficult

Use it carefully.


2️⃣ Factory Pattern

✅ Purpose

Create objects without exposing instantiation logic to the client.

✅ Problem Without Factory

Payment payment = new CreditCardPayment();

The client is tightly coupled to the implementation.

✅ Factory Implementation

public interface Payment {
    void pay();
}
public class CreditCardPayment implements Payment {
    public void pay() {
        System.out.println("Paid using Credit Card");
    }
}
public class UpiPayment implements Payment {
    public void pay() {
        System.out.println("Paid using UPI");
    }
}
public class PaymentFactory {

    public static Payment createPayment(String type) {

        if (type.equalsIgnoreCase("credit")) {
            return new CreditCardPayment();
        } 
        else if (type.equalsIgnoreCase("upi")) {
            return new UpiPayment();
        }

        throw new IllegalArgumentException("Invalid payment type");
    }
}

✅ Usage

Payment payment = PaymentFactory.createPayment("credit");
payment.pay();

✅ Now the client does not depend on concrete classes.


3️⃣ Builder Pattern

✅ Purpose

Create complex objects step-by-step.

Useful when:

  • Many optional parameters exist
  • Constructors become too large

❌ Problem

User user = new User("John", 25, "Delhi", "1234567890");

Hard to read and error-prone.

✅ Builder Implementation

public class User {

    private String name;
    private int age;
    private String city;

    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.city = builder.city;
    }

    public static class Builder {

        private String name;
        private int age;
        private String city;

        public Builder setName(String name) {
            this.name = name;
            return this;
        }

        public Builder setAge(int age) {
            this.age = age;
            return this;
        }

        public Builder setCity(String city) {
            this.city = city;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

✅ Usage

User user = new User.Builder()
                    .setName("John")
                    .setAge(25)
                    .setCity("Delhi")
                    .build();

✅ More readable
✅ Flexible
✅ Safer


4️⃣ Strategy Pattern

✅ Purpose

Define a family of algorithms and make them interchangeable.

Instead of:

if(paymentType.equals("credit")) { ... }
else if(paymentType.equals("upi")) { ... }

We delegate behavior to strategy classes.

✅ Strategy Interface

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

✅ Concrete Strategies

public class CreditCardPayment implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid " + amount + " using Credit Card");
    }
}
public class UpiPayment implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid " + amount + " using UPI");
    }
}

✅ Context Class

public class PaymentContext {

    private PaymentStrategy strategy;

    public PaymentContext(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void executePayment(double amount) {
        strategy.pay(amount);
    }
}

✅ Now behavior can change at runtime.


5️⃣ DTO (Data Transfer Object) Pattern

✅ Purpose

Transfer data between layers.

Example:

public class UserDTO {

    private String name;
    private String email;

    public UserDTO(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // getters
}

DTOs:

  • Prevent exposing internal models
  • Improve security
  • Reduce tight coupling

6️⃣ Layered Architecture

Professional applications follow layered structure:

Controller → Service → Repository

✅ Controller

Handles input/output (UI, API)

✅ Service

Contains business logic

✅ Repository

Handles database operations


✅ Example Structure

com.example.project
│
├── controller
├── service
├── repository
└── model

✅ Why This Matters

Without architecture:

  • Code becomes messy
  • Everything depends on everything
  • Testing becomes hard

With layered architecture:

  • Each class has one responsibility
  • Code is modular
  • Easier to extend
  • Easier to test

✅ Key Takeaway

Design patterns are tools.

Use them:

  • When they solve a real problem
  • When code becomes difficult to maintain
  • When flexibility is required

Do NOT use them just to “show knowledge”.

Clean code > Clever code.


This site uses Just the Docs, a documentation theme for Jekyll.