Design Patterns in Java – Singleton, Factory, Builder, Adapter, Observer, and More


Learn the most common design patterns in Java, including Singleton, Factory, Builder, Prototype, Adapter, Decorator, Strategy, and Observer. Understand how to implement them to solve common software design problems.

Design patterns are proven solutions to common problems encountered in software design. These patterns offer templates for solving issues in an efficient and reusable way. In Java, design patterns are widely used to write maintainable, scalable, and flexible code. This tutorial will introduce creational, structural, and behavioral design patterns and provide Java examples for each.

1. Singleton Design Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. This is often used for managing shared resources like database connections or thread pools.

Key Characteristics:

  1. Only one instance of the class exists.
  2. Provides a global point of access to that instance.
  3. The instance is created lazily (when required).

Example – Singleton in Java:


public class Singleton {

// The private static instance of the class
private static Singleton instance;

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

// Public method to provide access to the instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

Key Points:

  1. The private constructor ensures that no other instances can be created.
  2. The static getInstance() method is used to retrieve the single instance.

2. Factory Design Pattern

The Factory pattern provides a way to create objects without specifying the exact class of object that will be created. This is useful for creating families of related or dependent objects.

Key Characteristics:

  1. Defines an interface for creating an object.
  2. Subclasses implement the interface to create different types of objects.
  3. Promotes loose coupling between client classes and the objects they create.

Example – Factory in Java:


// Product interface
interface Shape {
void draw();
}

// Concrete classes implementing Shape interface
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}

class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}

// Factory class to create objects
class ShapeFactory {
public Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}
}

Key Points:

  1. The ShapeFactory provides an abstraction to create different types of shapes without specifying the exact class.

3. Builder Design Pattern

The Builder pattern is used to construct complex objects by separating the construction process from the representation. This pattern allows you to create an object step by step.

Key Characteristics:

  1. Splits the object creation process into multiple steps.
  2. Encapsulates the construction process of an object.
  3. Allows an object to be created in multiple representations.

Example – Builder in Java:


// Product class
class Computer {
private String CPU;
private String RAM;
private String storage;

public void setCPU(String CPU) {
this.CPU = CPU;
}

public void setRAM(String RAM) {
this.RAM = RAM;
}

public void setStorage(String storage) {
this.storage = storage;
}

@Override
public String toString() {
return "Computer [CPU=" + CPU + ", RAM=" + RAM + ", Storage=" + storage + "]";
}
}

// Builder class
class ComputerBuilder {
private Computer computer;

public ComputerBuilder() {
computer = new Computer();
}

public ComputerBuilder setCPU(String CPU) {
computer.setCPU(CPU);
return this;
}

public ComputerBuilder setRAM(String RAM) {
computer.setRAM(RAM);
return this;
}

public ComputerBuilder setStorage(String storage) {
computer.setStorage(storage);
return this;
}

public Computer build() {
return computer;
}
}

Key Points:

  1. The ComputerBuilder class helps create a complex Computer object step by step.
  2. Each method in the builder returns the builder itself to allow method chaining.

4. Prototype Design Pattern

The Prototype pattern allows you to clone objects, creating new objects that are copies of existing ones. It is often used when creating new instances of an object is expensive.

Key Characteristics:

  1. Clones objects to create new instances.
  2. Reduces the overhead of creating new objects from scratch.

Example – Prototype in Java:


// Prototype interface
interface Prototype {
Prototype clone();
}

// Concrete prototype
class Car implements Prototype {
private String make;
private String model;

public Car(String make, String model) {
this.make = make;
this.model = model;
}

@Override
public Prototype clone() {
return new Car(this.make, this.model);
}

@Override
public String toString() {
return "Car [Make=" + make + ", Model=" + model + "]";
}
}

Key Points:

  1. The clone() method creates a new object based on the current object, enabling quick object creation.

5. Adapter Design Pattern

The Adapter pattern is used to enable incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.

Key Characteristics:

  1. Allows classes with incompatible interfaces to work together.
  2. Converts the interface of a class into another interface that a client expects.

Example – Adapter in Java:


// Target interface
interface MediaPlayer {
void play(String audioType, String fileName);
}

// Adaptee class
class AudioPlayer implements MediaPlayer {
@Override
public void play(String audioType, String fileName) {
System.out.println("Playing audio: " + fileName);
}
}

// Adapter class
class MediaAdapter implements MediaPlayer {
private AudioPlayer audioPlayer;

public MediaAdapter(AudioPlayer audioPlayer) {
this.audioPlayer = audioPlayer;
}

@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("MP3")) {
audioPlayer.play(audioType, fileName);
}
}
}

Key Points:

  1. The MediaAdapter converts the AudioPlayer interface into the required format for the MediaPlayer interface.

6. Decorator Design Pattern

The Decorator pattern allows behavior to be added to an individual object dynamically, without affecting the behavior of other objects from the same class.

Key Characteristics:

  1. Enhances the functionality of an object at runtime.
  2. Provides an alternative to subclassing for extending functionality.

Example – Decorator in Java:


// Component interface
interface Car {
void assemble();
}

// Concrete Component
class BasicCar implements Car {
@Override
public void assemble() {
System.out.println("Basic Car.");
}
}

// Decorator class
class SportsCar implements Car {
private Car car;

public SportsCar(Car car) {
this.car = car;
}

@Override
public void assemble() {
this.car.assemble();
System.out.println("Adding sports features.");
}
}

Key Points:

  1. The SportsCar decorator enhances the BasicCar object by adding additional behavior.

7. Strategy Design Pattern

The Strategy pattern allows an algorithm’s behavior to be selected at runtime. It defines a family of algorithms and makes them interchangeable.

Key Characteristics:

  1. Allows the algorithm to be chosen at runtime.
  2. Promotes flexibility and decoupling.

Example – Strategy in Java:


// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}

// Concrete Strategy: PayPal
class PayPalStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paying " + amount + " using PayPal.");
}
}

// Concrete Strategy: CreditCard
class CreditCardStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paying " + amount + " using Credit Card.");
}
}

// Context class
class PaymentContext {
private PaymentStrategy strategy;

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

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

Key Points:

  1. The PaymentContext allows different PaymentStrategy objects to be used interchangeably.

8. Observer Design Pattern

The Observer pattern defines a one-to-many dependency between objects. When one object changes its state, all dependent objects are notified automatically.

Key Characteristics:

  1. A subject maintains a list of observers.
  2. Observers are notified of changes in the subject.

Example – Observer in Java:


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

// Subject class
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();
}
}
}

// Observer interface
interface Observer {
void update();
}

// Concrete Observer class
class ConcreteObserver implements Observer {
@Override
public void update() {
System.out.println("State updated!");
}
}

Key Points:

  1. The Subject maintains the list of Observers and notifies them of any changes.