In software engineering, design patterns are proven, general-purpose solutions to common problems encountered in object-oriented design. Rather than providing finished code, a design pattern offers a template or blueprint for solving a particular issue in a flexible, reusable way. Just as architects use blueprints to design buildings, developers use design patterns to structure software systems that are scalable, maintainable, and robust.
The concept of design patterns in software was popularized by the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software, authored by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides—collectively known as the Gang of Four (GoF). Their book introduced 23 classic design patterns and grouped them into three major categories:
These patterns have since become foundational knowledge for object-oriented developers and are widely taught and applied in modern software engineering.
Design patterns bring multiple advantages to the development process:
When used appropriately, design patterns lead to more readable, modular, and adaptable codebases, which are easier to debug, refactor, and test.
These deal with object instantiation in ways that promote flexibility and reuse. Instead of instantiating classes directly, creational patterns abstract the instantiation process. Examples include:
These focus on how classes and objects are composed to form larger structures. Structural patterns help ensure that components are organized efficiently and can work together. Examples include:
These deal with object interaction and responsibility. Behavioral patterns help manage communication and control flow among objects. Examples include:
One of the simplest design patterns is Singleton, which ensures that a class has only one instance and provides a global point of access to it. This is useful for managing shared resources such as a configuration manager or database connection.
public class ConfigurationManager {
private static ConfigurationManager instance;
private ConfigurationManager() { }
public static ConfigurationManager getInstance() {
if (instance == null) {
instance = new ConfigurationManager();
}
return instance;
}
}
With Singleton, the getInstance()
method always returns the same instance, ensuring consistency across the application.
Design patterns are essential tools in the object-oriented programmer’s toolbox. Far from being rigid formulas, they are flexible, proven strategies that help manage complexity and promote sound architectural decisions. By learning and applying design patterns thoughtfully, Java developers can write code that is easier to understand, reuse, and maintain—qualities that are critical in modern software development.
In the following sections, we’ll explore specific patterns from each category, how they’re implemented in Java, and when to apply them in real-world design scenarios.
Creational design patterns focus on how objects are created. They abstract the instantiation process, allowing systems to be more flexible and independent of the way objects are created, composed, and represented. In this section, we explore three essential creational patterns in Java: Singleton, Factory, and Builder.
Purpose: Ensure a class has only one instance and provide a global point of access to it.
public class Logger {
private static Logger instance;
private Logger() {
// private constructor prevents instantiation
}
public static Logger getInstance() {
if (instance == null) {
instance = new Logger(); // lazy initialization
}
return instance;
}
public void log(String message) {
System.out.println("Log: " + message);
}
}
Usage:
public class Main {
public static void main(String[] args) {
Logger logger = Logger.getInstance();
logger.log("Application started.");
}
}
Best Practice: Use lazy-loaded, thread-safe variants (e.g., using synchronized
, or static inner class).
Purpose: Define an interface for creating an object but allow subclasses to alter the type of objects that will be created. The Factory Method lets a class defer instantiation to subclasses or encapsulate logic in a factory class.
// Product interface
interface Notification {
void notifyUser();
}
// Concrete implementations
class EmailNotification implements Notification {
public void notifyUser() {
System.out.println("Sending Email Notification");
}
}
class SMSNotification implements Notification {
public void notifyUser() {
System.out.println("Sending SMS Notification");
}
}
// Factory
class NotificationFactory {
public static Notification createNotification(String type) {
if (type.equalsIgnoreCase("EMAIL")) {
return new EmailNotification();
} else if (type.equalsIgnoreCase("SMS")) {
return new SMSNotification();
}
throw new IllegalArgumentException("Unknown notification type");
}
}
Usage:
public class Main {
public static void main(String[] args) {
Notification notification = NotificationFactory.createNotification("SMS");
notification.notifyUser();
}
}
Best Practice: Keep factory logic clean, consider abstract factories for families of related objects.
Purpose: Separate the construction of a complex object from its representation, so the same construction process can create different representations.
public class Computer {
// Required parameters
private final String processor;
private final int ram;
// Optional parameters
private final boolean graphicsCard;
private final boolean bluetooth;
private Computer(Builder builder) {
this.processor = builder.processor;
this.ram = builder.ram;
this.graphicsCard = builder.graphicsCard;
this.bluetooth = builder.bluetooth;
}
public static class Builder {
private final String processor;
private final int ram;
private boolean graphicsCard = false;
private boolean bluetooth = false;
public Builder(String processor, int ram) {
this.processor = processor;
this.ram = ram;
}
public Builder graphicsCard(boolean value) {
this.graphicsCard = value;
return this;
}
public Builder bluetooth(boolean value) {
this.bluetooth = value;
return this;
}
public Computer build() {
return new Computer(this);
}
}
public void displayConfig() {
System.out.println("Processor: " + processor + ", RAM: " + ram +
"GB, Graphics Card: " + graphicsCard + ", Bluetooth: " + bluetooth);
}
}
Usage:
public class Main {
public static void main(String[] args) {
Computer customPC = new Computer.Builder("Intel i7", 16)
.graphicsCard(true)
.bluetooth(true)
.build();
customPC.displayConfig();
}
}
public class Main {
public static void main(String[] args) {
Computer customPC = new Computer.Builder("Intel i7", 16)
.graphicsCard(true)
.bluetooth(true)
.build();
customPC.displayConfig();
}
}
class Computer {
// Required parameters
private final String processor;
private final int ram;
// Optional parameters
private final boolean graphicsCard;
private final boolean bluetooth;
private Computer(Builder builder) {
this.processor = builder.processor;
this.ram = builder.ram;
this.graphicsCard = builder.graphicsCard;
this.bluetooth = builder.bluetooth;
}
public static class Builder {
private final String processor;
private final int ram;
private boolean graphicsCard = false;
private boolean bluetooth = false;
public Builder(String processor, int ram) {
this.processor = processor;
this.ram = ram;
}
public Builder graphicsCard(boolean value) {
this.graphicsCard = value;
return this;
}
public Builder bluetooth(boolean value) {
this.bluetooth = value;
return this;
}
public Computer build() {
return new Computer(this);
}
}
public void displayConfig() {
System.out.println("Processor: " + processor + ", RAM: " + ram +
"GB, Graphics Card: " + graphicsCard + ", Bluetooth: " + bluetooth);
}
}
Best Practice: Use for complex objects or those with many optional parameters (e.g., configuration, GUI components, data transfer objects).
Pattern | Best For | Avoid When... |
---|---|---|
Singleton | Single shared instance | Multiple instances are required |
Factory | Flexible, decoupled object creation | Object creation is trivial |
Builder | Complex object construction | Only simple constructors are needed |
Creational patterns offer flexible and reusable solutions to object creation problems in Java. Whether you need controlled instantiation (Singleton), decoupled creation logic (Factory), or step-by-step configuration (Builder), these patterns help build more modular, testable, and maintainable systems. By applying the appropriate creational pattern in the right context, developers can write cleaner and more expressive code that scales gracefully as complexity grows.
Structural design patterns are focused on how classes and objects are composed to form larger structures. These patterns simplify the relationships between entities, allowing developers to create flexible and extensible systems by enabling different parts of a program to work together more effectively.
This section covers three widely used structural patterns in Java: Adapter, Decorator, and Composite.
Intent: Convert the interface of a class into another interface that clients expect. The Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
When integrating legacy code or third-party libraries that don’t match your current interface expectations.
Suppose you have a legacy AudioPlayer
that plays .mp3
files, and a new media library supports .mp4
. You want your system to adapt the new format without modifying the existing player.
// Existing interface
interface MediaPlayer {
void play(String audioType, String fileName);
}
// New library with a different interface
class AdvancedMediaPlayer {
public void playMp4(String fileName) {
System.out.println("Playing MP4 file: " + fileName);
}
}
// Adapter to bridge both
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer = new AdvancedMediaPlayer();
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer.playMp4(fileName);
} else {
System.out.println("Unsupported format: " + audioType);
}
}
}
// Client
class AudioPlayer implements MediaPlayer {
private MediaAdapter adapter = new MediaAdapter();
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file: " + fileName);
} else {
adapter.play(audioType, fileName);
}
}
}
Usage:
AudioPlayer player = new AudioPlayer();
player.play("mp3", "song.mp3");
player.play("mp4", "video.mp4");
Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
When you want to add functionality to objects without changing their class or creating a large inheritance hierarchy.
// Core interface
interface Coffee {
String getDescription();
double cost();
}
// Concrete component
class SimpleCoffee implements Coffee {
public String getDescription() { return "Simple Coffee"; }
public double cost() { return 5.0; }
}
// Decorator base class
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
}
// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return decoratedCoffee.getDescription() + ", Milk";
}
public double cost() {
return decoratedCoffee.cost() + 1.5;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return decoratedCoffee.getDescription() + ", Sugar";
}
public double cost() {
return decoratedCoffee.cost() + 0.5;
}
}
Usage:
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription()); // Simple Coffee, Milk, Sugar
System.out.println("Cost: $" + coffee.cost()); // Cost: $7.0
// Core interface
interface Coffee {
String getDescription();
double cost();
}
// Concrete component
class SimpleCoffee implements Coffee {
public String getDescription() { return "Simple Coffee"; }
public double cost() { return 5.0; }
}
// Decorator base class
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
}
// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return decoratedCoffee.getDescription() + ", Milk";
}
public double cost() {
return decoratedCoffee.cost() + 1.5;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return decoratedCoffee.getDescription() + ", Sugar";
}
public double cost() {
return decoratedCoffee.cost() + 0.5;
}
}
public class DecoratorDemo {
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription()); // Simple Coffee, Milk, Sugar
System.out.println("Cost: $" + coffee.cost()); // Cost: $7.0
}
}
Intent: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions uniformly.
When building hierarchies such as graphical components (buttons inside panels), file systems (folders containing files), or organizational charts.
// Component
interface FileSystem {
void ls();
}
// Leaf
class File implements FileSystem {
private String name;
public File(String name) { this.name = name; }
public void ls() {
System.out.println("File: " + name);
}
}
// Composite
class Directory implements FileSystem {
private String name;
private List<FileSystem> contents = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void add(FileSystem fs) {
contents.add(fs);
}
public void ls() {
System.out.println("Directory: " + name);
for (FileSystem fs : contents) {
fs.ls();
}
}
}
Usage:
FileSystem file1 = new File("readme.txt");
FileSystem file2 = new File("data.csv");
Directory dir = new Directory("MyDocs");
dir.add(file1);
dir.add(file2);
Directory root = new Directory("Root");
root.add(dir);
root.ls();
Output:
Directory: Root
Directory: MyDocs
File: readme.txt
File: data.csv
import java.util.ArrayList;
import java.util.List;
// Component
interface FileSystem {
void ls();
}
// Leaf
class File implements FileSystem {
private String name;
public File(String name) { this.name = name; }
public void ls() {
System.out.println("File: " + name);
}
}
// Composite
class Directory implements FileSystem {
private String name;
private List<FileSystem> contents = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void add(FileSystem fs) {
contents.add(fs);
}
public void ls() {
System.out.println("Directory: " + name);
for (FileSystem fs : contents) {
fs.ls();
}
}
}
public class CompositeDemo {
public static void main(String[] args) {
FileSystem file1 = new File("readme.txt");
FileSystem file2 = new File("data.csv");
Directory dir = new Directory("MyDocs");
dir.add(file1);
dir.add(file2);
Directory root = new Directory("Root");
root.add(dir);
root.ls();
}
}
Pattern | Intent | When to Use |
---|---|---|
Adapter | Convert incompatible interfaces | Bridging legacy code or APIs |
Decorator | Add behavior dynamically | Customize functionality without changing class |
Composite | Treat groups and objects uniformly | Model tree-like structures (e.g., UI, filesystems) |
Structural design patterns like Adapter, Decorator, and Composite offer powerful tools for organizing code, reducing coupling, and enhancing system flexibility. These patterns help developers build systems that are easier to extend, maintain, and scale—core goals of object-oriented design. By mastering these patterns, Java developers can craft more modular, adaptable, and reusable code architectures.
Behavioral design patterns are concerned with how objects interact and communicate to fulfill responsibilities. These patterns increase flexibility in carrying out behaviors and decouple sender and receiver objects. In this section, we’ll explore three widely used behavioral patterns in Java: Strategy, Observer, and Command.
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy allows an algorithm’s behavior to be selected at runtime.
When different algorithms can be applied interchangeably without altering the context class using them—e.g., sorting strategies or payment methods.
// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid $" + amount + " using Credit Card.");
}
}
class PayPalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid $" + amount + " using PayPal.");
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
Usage:
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(100); // Paid $100 using PayPal
// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid $" + amount + " using Credit Card.");
}
}
class PayPalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid $" + amount + " using PayPal.");
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
if (paymentStrategy == null) {
System.out.println("Payment strategy not set!");
} else {
paymentStrategy.pay(amount);
}
}
}
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(100); // Output: Paid $100 using PayPal
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(250); // Output: Paid $250 using Credit Card
}
}
Intent: Define a one-to-many dependency so that when one object changes state, all its dependents are notified and updated automatically.
Widely used in event-driven systems, GUIs, and model-view-controller (MVC) frameworks—e.g., updating multiple UI components when data changes.
// Observer interface
interface Observer {
void update(String message);
}
// Subject interface
interface Subject {
void attach(Observer o);
void detach(Observer o);
void notifyObservers();
}
// Concrete subject
class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
public void setNews(String news) {
this.news = news;
notifyObservers();
}
public void attach(Observer o) { observers.add(o); }
public void detach(Observer o) { observers.remove(o); }
public void notifyObservers() {
for (Observer o : observers) {
o.update(news);
}
}
}
// Concrete observer
class NewsChannel implements Observer {
private String name;
public NewsChannel(String name) {
this.name = name;
}
public void update(String message) {
System.out.println(name + " received: " + message);
}
}
Usage:
NewsAgency agency = new NewsAgency();
agency.attach(new NewsChannel("Channel A"));
agency.attach(new NewsChannel("Channel B"));
agency.setNews("Breaking: Strategy Pattern Deployed!");
Output:
Channel A received: Breaking: Strategy Pattern Deployed!
Channel B received: Breaking: Strategy Pattern Deployed!
import java.util.ArrayList;
import java.util.List;
// Observer interface
interface Observer {
void update(String message);
}
// Subject interface
interface Subject {
void attach(Observer o);
void detach(Observer o);
void notifyObservers();
}
// Concrete subject
class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
public void setNews(String news) {
this.news = news;
notifyObservers();
}
public void attach(Observer o) { observers.add(o); }
public void detach(Observer o) { observers.remove(o); }
public void notifyObservers() {
for (Observer o : observers) {
o.update(news);
}
}
}
// Concrete observer
class NewsChannel implements Observer {
private String name;
public NewsChannel(String name) {
this.name = name;
}
public void update(String message) {
System.out.println(name + " received: " + message);
}
}
public class ObserverDemo {
public static void main(String[] args) {
NewsAgency agency = new NewsAgency();
agency.attach(new NewsChannel("Channel A"));
agency.attach(new NewsChannel("Channel B"));
agency.setNews("Breaking: Strategy Pattern Deployed!");
}
}
Intent: Encapsulate a request as an object, thereby allowing for parameterization of clients with queues, logs, and undo/redo operations.
When you want to parameterize operations (e.g., buttons triggering commands), support undo functionality, or queue operations.
// Command interface
interface Command {
void execute();
}
// Receiver
class Light {
public void turnOn() {
System.out.println("Light turned ON");
}
public void turnOff() {
System.out.println("Light turned OFF");
}
}
// Concrete commands
class TurnOnCommand implements Command {
private Light light;
public TurnOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOn();
}
}
class TurnOffCommand implements Command {
private Light light;
public TurnOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOff();
}
}
// Invoker
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
Usage:
Light light = new Light();
Command on = new TurnOnCommand(light);
Command off = new TurnOffCommand(light);
RemoteControl remote = new RemoteControl();
remote.setCommand(on);
remote.pressButton(); // Light turned ON
remote.setCommand(off);
remote.pressButton(); // Light turned OFF
// Command interface
interface Command {
void execute();
}
// Receiver
class Light {
public void turnOn() {
System.out.println("Light turned ON");
}
public void turnOff() {
System.out.println("Light turned OFF");
}
}
// Concrete commands
class TurnOnCommand implements Command {
private Light light;
public TurnOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOn();
}
}
class TurnOffCommand implements Command {
private Light light;
public TurnOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOff();
}
}
// Invoker
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
public class CommandDemo {
public static void main(String[] args) {
Light light = new Light();
Command on = new TurnOnCommand(light);
Command off = new TurnOffCommand(light);
RemoteControl remote = new RemoteControl();
remote.setCommand(on);
remote.pressButton(); // Light turned ON
remote.setCommand(off);
remote.pressButton(); // Light turned OFF
}
}
Pattern | Intent | Ideal For |
---|---|---|
Strategy | Choose algorithm dynamically | Behavior switching (e.g., different logics) |
Observer | Notify subscribers when subject changes | Event-driven systems, UI updates |
Command | Encapsulate requests as objects | Queues, undo/redo, remote execution |
Behavioral design patterns such as Strategy, Observer, and Command are essential tools for organizing object communication in a clean and scalable way. They help developers decouple objects, promote flexibility, and design systems that are easier to extend and maintain. When used appropriately, these patterns can significantly enhance the responsiveness, modularity, and clarity of Java applications.