Top 5 Essential Design Patterns in Software Development with Java & C# Examples

Design patterns are proven solutions to common software design problems. They help developers create scalable, maintainable, and efficient software systems. Among the many design patterns, five stand out as essential for any software engineer. This article explores these critical design patterns with real-world examples and best practices.

1. Singleton Pattern

Purpose: Ensures that a class has only one instance and provides a global point of access to it.

Example using Enum (Java):

public enum SingletonEnum {
    INSTANCE;

    public void someMethod() {
        System.out.println("Singleton using Enum");
    }
}Code language: PHP (php)

Example using Enum (C#):

public sealed class SingletonEnum {
    private static readonly SingletonEnum instance = new SingletonEnum();

    private SingletonEnum() {}

    public static SingletonEnum Instance {
        get { return instance; }
    }

    public void SomeMethod() {
        Console.WriteLine("Singleton using Enum");
    }
}Code language: PHP (php)

Use Cases:

  • Managing shared resources (e.g., database connections, logging mechanisms, configuration settings)
  • Implementing caches or thread pools

Example in Java ( using null check and synchronized block):

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}Code language: PHP (php)

Example in C#:

public sealed class Singleton {
    private static readonly object lockObj = new object();
    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton Instance {
        get {
            lock (lockObj) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
            return instance;
        }
    }
}Code language: PHP (php)

Best Practices:

  • Use double-checked locking for thread safety.
  • Consider using an enum-based singleton for a simpler implementation.

2. Factory Method Pattern

Purpose: Defines an interface for creating objects but allows subclasses to alter the type of objects that will be created.

Example in Java:

abstract class Product {
    abstract void printPrice();
}

class ConcreteProductA extends Product {
    @Override
    void printPrice() {
        System.out.println("Price of Product A");
    }
}

class ConcreteProductB extends Product {
    @Override
    void printPrice() {
        System.out.println("Price of Product B");
    }
}

class Factory {
    static Product createProduct(String type) {
        if (type.equals("A")) {
            return new ConcreteProductA();
        } else if (type.equals("B")) {
            return new ConcreteProductB();
        }
        throw new IllegalArgumentException("Unknown product type");
    }
}Code language: JavaScript (javascript)

Example in C#:

abstract class Product {
    public abstract void Operation();
}

class ConcreteProductA : Product {
    public override void Operation() {
        Console.WriteLine("Product A");
    }
}

class ConcreteProductB : Product {
    public override void Operation() {
        Console.WriteLine("Product B");
    }
}

class Factory {
    public static Product CreateProduct(string type) {
        return type switch {
            "A" => new ConcreteProductA(),
            "B" => new ConcreteProductB(),
            _ => throw new ArgumentException("Unknown product type")
        };
    }
}

3. Observer Pattern

Purpose: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically.

Example in Java:

import java.util.ArrayList;
import java.util.List;

interface Observer {
    void update(String message);
}

class ConcreteObserver implements Observer {
    private String name;

    public ConcreteObserver(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + " received update: " + message);
    }
}

class Subject {
    private List<Observer> observers = new ArrayList<>();

    void subscribe(Observer observer) {
        observers.add(observer);
    }

    void unsubscribe(Observer observer) {
        observers.remove(observer);
    }

    void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}Code language: JavaScript (javascript)

Example in C#:

using System;
using System.Collections.Generic;

interface IObserver {
    void Update(string message);
}

class ConcreteObserver : IObserver {
    private string name;

    public ConcreteObserver(string name) {
        this.name = name;
    }

    public void Update(string message) {
        Console.WriteLine($"{name} received update: {message}");
    }
}

class Subject {
    private List<IObserver> observers = new List<IObserver>();

    public void Subscribe(IObserver observer) {
        observers.Add(observer);
    }

    public void Unsubscribe(IObserver observer) {
        observers.Remove(observer);
    }

    public void NotifyObservers(string message) {
        foreach (var observer in observers) {
            observer.Update(message);
        }
    }
}Code language: HTML, XML (xml)

4. Strategy Pattern

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows a class’s behavior to be selected at runtime.

Instead of implementing multiple algorithms within a class, the Strategy Pattern delegates responsibility to separate classes, each representing a different strategy.

Key Components of Strategy Pattern

1. Strategy Interface – Defines a common interface for all supported strategies.

2. Concrete Strategies – Implements different variations of the algorithm.

3. Context Class – Uses a Strategy object to invoke a specific behavior.

When to Use the Strategy Pattern?

Use the Strategy Pattern when:

• You have multiple algorithms that can be used interchangeably.

• You want to avoid if-else or switch-case statements to determine which algorithm to use.

• You need to follow the Open-Closed Principle (allow extensions without modifying existing code).

• Behavior needs to be dynamically chosen at runtime.

Example in Java:

interface PaymentStrategy {
    void pay(int amount);
}
// Concrete Strategy 1: CreditCard Payment
class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using Credit Card.");
    }
}
// Concrete Strategy 2: PayPal Payment
class PayPalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using PayPal.");
    }
}

// Concrete Strategy 3: Bitcoin Payment
class BitcoinPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using Bitcoin.");
    }
}
class PaymentContext {
    private PaymentStrategy strategy;

    // Method to set the strategy at runtime
    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    // Executes the payment using the selected strategy
    public void executePayment(int amount) {
        if (strategy == null) {
            System.out.println("No payment strategy selected!");
            return;
        }
        strategy.pay(amount);
    }
}Code language: PHP (php)


Now, let’s create a test class to demonstrate how we can dynamically switch between payment strategies.

public class StrategyPatternExample {
  public static void main(String[] args) {
    PaymentContext context = new PaymentContext();
    // Using Credit Card Payment Strategy
    context.setStrategy(new CreditCardPayment());
    context.executePayment(100);

    // Switching to PayPal Payment Strategy
    context.setStrategy(new PayPalPayment());
    context.executePayment(200);

    // Switching to Bitcoin Payment Strategy
    context.setStrategy(new BitcoinPayment());
    context.executePayment(300);
 }
}Code language: JavaScript (javascript)

Example in C#:

interface PaymentStrategy {
    void Pay(int amount);
}

class CreditCardPayment : PaymentStrategy {
    public void Pay(int amount) {
        Console.WriteLine($"Paid {amount} using Credit Card.");
    }
}
//Add rest of the code based on java example

5. Decorator Pattern

Allows behavior to be added to an individual object dynamically without modifying its structure.

It is an alternative to subclassing, providing a flexible way to extend functionality by wrapping objects inside other objects.

Key Characteristics of the Decorator Pattern:

• Used to dynamically add new behavior to objects.

• Avoids creating a large number of subclasses.

• Uses composition over inheritance by wrapping objects.

• Adheres to the Open-Closed Principle (open for extension, closed for modification).

Key Components of the Decorator Pattern

1. Component (Interface or Abstract Class) – The base interface for objects that can have responsibilities added to them dynamically.

2. Concrete Component – A class that implements the Component interface (base functionality).

3. Decorator (Abstract Class) – Wraps the component and implements the same interface.

4. Concrete Decorators – Extends the Decorator class and adds extra functionality.

Real-World Example: Coffee Shop

Imagine a coffee shop where customers can order a basic coffee and customize it with additional features like milk, sugar, or caramel. Instead of creating separate classes for every coffee combination, we use the Decorator Pattern to add ingredients dynamically.

Example in Java:

// Component Interface
interface Coffee {
    String getDescription();
    double getCost();
}

class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple Coffee";
    }

    @Override
    public double getCost() {
        return 5.00;
    }
}
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }

    @Override
    public double getCost() {
        return decoratedCoffee.getCost();
    }
}
// Concrete Decorator 1: Adds Milk
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", Milk";
    }

    @Override
    public double getCost() {
        return super.getCost() + 1.50;
    }
}

// Concrete Decorator 2: Adds Sugar
class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", Sugar";
    }

    @Override
    public double getCost() {
        return super.getCost() + 0.50;
    }
}
//Using the pattern
public class DecoratorPatternExample {
    public static void main(String[] args) {
        Coffee simpleCoffee = new SimpleCoffee();
        System.out.println(simpleCoffee.getDescription() + " -> Cost: $" + simpleCoffee.getCost());

        // Add Milk
        Coffee milkCoffee = new MilkDecorator(simpleCoffee);
        System.out.println(milkCoffee.getDescription() + " -> Cost: $" + milkCoffee.getCost());

        // Add Sugar
        Coffee sugarMilkCoffee = new SugarDecorator(milkCoffee);
        System.out.println(sugarMilkCoffee.getDescription() + " -> Cost: $" + sugarMilkCoffee.getCost());
    }
}Code language: PHP (php)

Example in C#:

interface Component {
    void Execute();
}

class ConcreteComponent : Component {
    public void Execute() {
        Console.WriteLine("Executing base component");
    }
}
//Add rest of the code based on java example

Conclusion

Understanding and applying design patterns effectively can significantly improve software architecture. The Singleton, Factory Method, Observer, Strategy, and Decorator patterns are among the most valuable for building maintainable and scalable applications. By mastering these patterns, developers can design more robust and flexible systems that are easier to extend and maintain.

Comments

One response to “Top 5 Essential Design Patterns in Software Development with Java & C# Examples”

  1. pramod Avatar
    pramod

    Nice one.


 We break down complex topics into clear, actionable content. Built for developers, learners, and curious minds.


Socail Connect

theStemBookDev

about science, tech, engineering , programming and more