Exploring 5 Widely Used Design Patterns in Java with Real-World Examples

Naveen Kumar Ravi
3 min readJun 25, 2023

--

Introduction:

Design patterns play a crucial role in software development, enabling developers to create reusable and maintainable code. In this article, we will dive into five popular design patterns in Java, highlighting their key concepts and demonstrating their practical applications with real-world examples. Understanding these patterns will empower you to write cleaner, more efficient code.

Singleton Pattern:

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is beneficial when you need a single, shared instance of an object throughout your application. Real-world examples include database connections, thread pools, and logging systems.

public class Singleton {
private static Singleton instance;

private Singleton() {
// Private constructor to prevent instantiation.
}

public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

Factory Pattern:

The Factory pattern provides an interface for creating objects without specifying their concrete classes. It encapsulates object creation logic, allowing flexibility and loose coupling. Consider scenarios where you need to create different types of objects based on certain conditions or parameters.

public interface Vehicle {
void manufacture();
}

public class Car implements Vehicle {
@Override
public void manufacture() {
System.out.println("Car manufactured.");
}
}

public class Bike implements Vehicle {
@Override
public void manufacture() {
System.out.println("Bike manufactured.");
}
}

public class VehicleFactory {
public static Vehicle createVehicle(String type) {
if (type.equalsIgnoreCase("car")) {
return new Car();
} else if (type.equalsIgnoreCase("bike")) {
return new Bike();
}
return null;
}
}

Observer Pattern:

The Observer pattern establishes a one-to-many dependency between objects, where changes in one object trigger updates in multiple dependent objects. This pattern is useful when you need to maintain consistency among objects or notify them about specific events.

public interface Observer {
void update();
}

public class StockObserver implements Observer {
@Override
public void update() {
System.out.println("Stock price updated. Refreshing stock market data...");
}
}

public class WeatherObserver implements Observer {
@Override
public void update() {
System.out.println("Weather forecast updated. Displaying new weather information...");
}
}

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

public void addObserver(Observer observer) {
observers.add(observer);
}

public void removeObserver(Observer observer) {
observers.remove(observer);
}

public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}

Decorator Pattern:

The Decorator pattern enables dynamic behavior extension of an object by wrapping it with additional functionality at runtime. This pattern promotes the principle of open-closed design, allowing new behaviors to be added without modifying existing code.

public interface UIComponent {
void draw();
}

public class Button implements UIComponent {
@Override
public void draw() {
System.out.println("Drawing a button");
}
}

public abstract class UIComponentDecorator implements UIComponent {
protected UIComponent decoratedComponent;

public UIComponentDecorator(UIComponent decoratedComponent) {
this.decoratedComponent = decoratedComponent;
}

@Override
public void draw() {
decoratedComponent.draw();
}
}

public class BorderDecorator extends UIComponentDecorator {
public BorderDecorator(UIComponent decoratedComponent) {
super(decoratedComponent);
}

@Override
public void draw() {
super.draw();
drawBorder();
}

private void drawBorder() {
System.out.println("Drawing a border around the component");
}
}

Strategy Pattern:

The Strategy pattern defines a family of interchangeable algorithms and encapsulates each algorithm separately. It allows you to dynamically switch between different algorithms based on specific requirements.

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

public class CreditCardPaymentStrategy implements PaymentStrategy {
private String cardNumber;
private String cvv;

public CreditCardPaymentStrategy(String cardNumber, String cvv) {
this.cardNumber = cardNumber;
this.cvv = cvv;
}

@Override
public void pay(double amount) {
System.out.println("Paying " + amount + " with credit card " + cardNumber);
}
}

public class PayPalPaymentStrategy implements PaymentStrategy {
private String email;
private String password;

public PayPalPaymentStrategy(String email, String password) {
this.email = email;
this.password = password;
}

@Override
public void pay(double amount) {
System.out.println("Paying " + amount + " with PayPal account " + email);
}
}

public class PaymentContext {
private PaymentStrategy paymentStrategy;

public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}

public void processPayment(double amount) {
paymentStrategy.pay(amount);
}
}

Conclusion:

Design patterns are invaluable tools for software developers, offering proven solutions to common programming challenges. In this article, we explored five widely used design patterns in Java — Singleton, Factory, Observer, Decorator, and Strategy — providing real-world examples and use cases for each pattern. By understanding and applying these patterns, you can enhance the maintainability, reusability, and flexibility of your Java code. Start leveraging these powerful design patterns in your projects and unlock their benefits today!

--

--

Naveen Kumar Ravi
Naveen Kumar Ravi

Written by Naveen Kumar Ravi

Technical Architect | Java Full stack Developer with 9+ years of hands-on experience designing, developing, and implementing applications.