Design Patterns in Java
This is a continuation of the previous discussion on Introduction to Design Patterns
3.3 Behavioral Design Patterns
- Focus: Communication between objects.
- Why: In real-world apps, objects often need to collaborate while keeping loose coupling.
- Solution: Defines interaction, responsibility, and communication patterns.
Patterns Included:
A. Chain of Responsibility Pattern
Definition:
Gives multiple objects a chance to handle a request without knowing the request’s final receiver.
The request is passed along a chain until one object handles it.
When to Use:
- When multiple objects can handle a request.
- To avoid coupling sender with receiver.
Real-World Example:
- Customer support system → Query passes from junior support → manager → director, until solved.
Java Example:
// Handler
abstract class Handler {
protected Handler nextHandler;
public void setNextHandler(Handler handler) {
this.nextHandler = handler;
}
public abstract void handleRequest(String request);
}
// Concrete Handlers
class Manager extends Handler {
public void handleRequest(String request) {
if (request.equals("leave")) {
System.out.println("Manager approves leave.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
class Director extends Handler {
public void handleRequest(String request) {
if (request.equals("salary hike")) {
System.out.println("Director approves salary hike.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
Handler manager = new Manager();
Handler director = new Director();
manager.setNextHandler(director);
manager.handleRequest("leave");
manager.handleRequest("salary hike");
}
}
✅ Output:
Manager approves leave.
Director approves salary hike.
Advantages:
- Decouples sender from receiver.
- Flexible chain configuration.
Disadvantages:
- Request may go unhandled if no object takes responsibility.
B. Command Pattern
Definition:
Encapsulates a request as an object, allowing undo/redo functionality.
When to Use:
- To implement undo/redo.
- To queue requests, log operations.
Real-World Example:
- Remote control → Each button is a command (turn on TV, change channel, etc.).
Java Example:
// Command Interface
interface Command {
void execute();
}
// Receiver
class Light {
public void on() { System.out.println("Light is ON"); }
public void off() { System.out.println("Light is OFF"); }
}
// Concrete Commands
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) { this.light = light; }
public void execute() { light.on(); }
}
class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) { this.light = light; }
public void execute() { light.off(); }
}
// Invoker
class RemoteControl {
private Command command;
public void setCommand(Command command) { this.command = command; }
public void pressButton() { command.execute(); }
}
// Usage
public class Main {
public static void main(String[] args) {
Light light = new Light();
Command lightOn = new LightOnCommand(light);
Command lightOff = new LightOffCommand(light);
RemoteControl remote = new RemoteControl();
remote.setCommand(lightOn);
remote.pressButton();
remote.setCommand(lightOff);
remote.pressButton();
}
}
✅ Output:
Light is ON
Light is OFF
Advantages:
- Supports undo/redo easily.
- Decouples sender and receiver.
Disadvantages:
- Large number of command classes for different actions.
C. Interpreter Pattern
Definition:
Defines a grammar for a language and provides an interpreter to evaluate expressions in that language.
When to Use:
- To interpret expressions of a language (e.g., SQL, regex, calculators).
Real-World Example:
- Regex engine → Interprets search patterns.
Java Example:
// Expression
interface Expression {
boolean interpret(String context);
}
// Terminal Expression
class TerminalExpression implements Expression {
private String data;
public TerminalExpression(String data) { this.data = data; }
public boolean interpret(String context) {
return context.contains(data);
}
}
// Or Expression
class OrExpression implements Expression {
private Expression expr1;
private Expression expr2;
public OrExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
public boolean interpret(String context) {
return expr1.interpret(context) || expr2.interpret(context);
}
}
// Usage
public class Main {
public static void main(String[] args) {
Expression java = new TerminalExpression("Java");
Expression python = new TerminalExpression("Python");
Expression orExpr = new OrExpression(java, python);
System.out.println(orExpr.interpret("I love Java")); // true
System.out.println(orExpr.interpret("I love C++")); // false
}
}
✅ Output:
true
false
Advantages:
- Good for designing small domain-specific languages (DSLs).
Disadvantages:
- Hard to maintain with complex grammars.
D. Iterator Pattern
Definition:
Provides a way to access elements of a collection sequentially without exposing its underlying structure.
When to Use:
- When collections need multiple traversal methods.
- When you want to hide collection’s internal structure.
Real-World Example:
- Remote control playlist → Next/Previous song without knowing playlist internals.
Java Example:
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
class NameRepository implements Iterable {
private List names = new ArrayList<>();
public void addName(String name) {
names.add(name);
}
public Iterator iterator() {
return names.iterator();
}
}
// Usage
public class Main {
public static void main(String[] args) {
NameRepository repo = new NameRepository();
repo.addName("Alice");
repo.addName("Bob");
repo.addName("Charlie");
for (String name : repo) {
System.out.println(name);
}
}
}
✅ Output:
Alice
Bob
Charlie
Advantages:
- Supports multiple traversal methods.
- Hides collection structure.
Disadvantages:
- Slight overhead in creating iterator objects.
E. Mediator Pattern
Definition:
The Mediator pattern defines an object that coordinates communication between multiple objects (colleagues) without them referring to each other directly.
When to Use:
- To reduce complex communication between many objects.
- To centralize control in a mediator.
Real-World Example:
- Air Traffic Control (ATC) → Planes (colleagues) don’t communicate directly, they go through ATC (mediator).
Java Example:
import java.util.ArrayList;
import java.util.List;
// Mediator
interface ChatMediator {
void sendMessage(String msg, User user);
void addUser(User user);
}
// Concrete Mediator
class ChatRoom implements ChatMediator {
private List users = new ArrayList<>();
public void addUser(User user) { users.add(user); }
public void sendMessage(String msg, User user) {
for (User u : users) {
if (u != user) {
u.receive(msg);
}
}
}
}
// Colleague
abstract class User {
protected ChatMediator mediator;
protected String name;
public User(ChatMediator mediator, String name) {
this.mediator = mediator;
this.name = name;
}
abstract void send(String msg);
abstract void receive(String msg);
}
// Concrete Colleague
class ConcreteUser extends User {
public ConcreteUser(ChatMediator mediator, String name) {
super(mediator, name);
}
public void send(String msg) {
System.out.println(name + " sends: " + msg);
mediator.sendMessage(msg, this);
}
public void receive(String msg) {
System.out.println(name + " receives: " + msg);
}
}
// Usage
public class Main {
public static void main(String[] args) {
ChatMediator chat = new ChatRoom();
User user1 = new ConcreteUser(chat, "Alice");
User user2 = new ConcreteUser(chat, "Bob");
User user3 = new ConcreteUser(chat, "Charlie");
chat.addUser(user1);
chat.addUser(user2);
chat.addUser(user3);
user1.send("Hello, everyone!");
}
}
✅ Output:
Alice sends: Hello, everyone!
Bob receives: Hello, everyone!
Charlie receives: Hello, everyone!
Advantages:
- Reduces direct dependencies.
- Simplifies communication logic.
Disadvantages:
- Mediator can become very complex (God object).
F. Memento Pattern
Definition:
Captures and externalizes an object’s internal state so it can be restored later, without exposing its implementation.
When to Use:
- To implement undo/redo.
- To rollback to a previous state.
Real-World Example:
- Text editor undo button → Saves previous versions.
Java Example:
// Memento
class Memento {
private String state;
public Memento(String state) { this.state = state; }
public String getState() { return state; }
}
// Originator
class Editor {
private String content;
public void setContent(String content) { this.content = content; }
public String getContent() { return content; }
public Memento save() { return new Memento(content); }
public void restore(Memento memento) { content = memento.getState(); }
}
// Caretaker
class History {
private java.util.Stack states = new java.util.Stack<>();
public void push(Memento m) { states.push(m); }
public Memento pop() { return states.pop(); }
}
// Usage
public class Main {
public static void main(String[] args) {
Editor editor = new Editor();
History history = new History();
editor.setContent("Version 1");
history.push(editor.save());
editor.setContent("Version 2");
history.push(editor.save());
editor.setContent("Version 3");
System.out.println("Current: " + editor.getContent());
editor.restore(history.pop());
System.out.println("After undo: " + editor.getContent());
editor.restore(history.pop());
System.out.println("After undo: " + editor.getContent());
}
}
✅ Output:
Current: Version 3
After undo: Version 2
After undo: Version 1
Advantages:
- Provides undo/redo functionality.
- Encapsulates state without breaking encapsulation.
Disadvantages:
- Memory overhead if many states are stored.
G. Observer Pattern
Definition:
Defines a one-to-many relationship: when one object (subject) changes state, all its dependents (observers) are notified automatically.
When to Use:
- Event-driven systems.
- Publish-subscribe model.
Real-World Example:
- YouTube notifications → Subscribers get notified when a new video is uploaded.
Java Example:
import java.util.ArrayList;
import java.util.List;
// Subject
class Channel {
private List subscribers = new ArrayList<>();
private String video;
public void subscribe(Subscriber sub) { subscribers.add(sub); }
public void unsubscribe(Subscriber sub) { subscribers.remove(sub); }
public void uploadVideo(String video) {
this.video = video;
notifyAllSubscribers();
}
private void notifyAllSubscribers() {
for (Subscriber sub : subscribers) {
sub.update(video);
}
}
}
// Observer
interface Subscriber {
void update(String video);
}
// Concrete Observer
class User implements Subscriber {
private String name;
public User(String name) { this.name = name; }
public void update(String video) {
System.out.println(name + " notified: New video uploaded - " + video);
}
}
// Usage
public class Main {
public static void main(String[] args) {
Channel channel = new Channel();
User u1 = new User("Alice");
User u2 = new User("Bob");
channel.subscribe(u1);
channel.subscribe(u2);
channel.uploadVideo("Design Patterns in Java");
}
}
✅ Output:
Alice notified: New video uploaded - Design Patterns in Java
Bob notified: New video uploaded - Design Patterns in Java
Advantages:
- Loose coupling between subject and observers.
- Useful for event-driven systems.
Disadvantages:
- May cause unexpected updates if not managed properly.
H. State Pattern
Definition:
Allows an object to change its behavior when its internal state changes.
The object appears to change its class.
When to Use:
- When an object’s behavior depends on its state.
- To avoid multiple conditionals (if-else/switch).
Real-World Example:
- ATM machine → Different behavior when card is inserted vs. not inserted.
Java Example:
// State
interface State {
void insertCard();
void ejectCard();
void enterPin();
}
// Concrete States
class NoCardState implements State {
public void insertCard() { System.out.println("Card inserted."); }
public void ejectCard() { System.out.println("No card to eject."); }
public void enterPin() { System.out.println("Insert card first."); }
}
class HasCardState implements State {
public void insertCard() { System.out.println("Card already inserted."); }
public void ejectCard() { System.out.println("Card ejected."); }
public void enterPin() { System.out.println("PIN entered."); }
}
// Context
class ATM {
private State state;
public ATM() { state = new NoCardState(); }
public void setState(State state) { this.state = state; }
public void insertCard() { state.insertCard(); setState(new HasCardState()); }
public void ejectCard() { state.ejectCard(); setState(new NoCardState()); }
public void enterPin() { state.enterPin(); }
}
// Usage
public class Main {
public static void main(String[] args) {
ATM atm = new ATM();
atm.insertCard();
atm.enterPin();
atm.ejectCard();
}
}
✅ Output:
Card inserted.
PIN entered.
Card ejected.
Advantages:
- Removes large if-else or switch statements.
- Organizes code by encapsulating states.
Disadvantages:
- May require many state classes.
I. Strategy Pattern
Definition:
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
When to Use:
- When multiple algorithms are possible for a task.
- To switch behavior dynamically without changing client code.
Real-World Example:
- Payment system → Pay with Credit Card, PayPal, UPI (different strategies).
Java Example:
// Strategy
interface PaymentStrategy {
void pay(int amount);
}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) { this.cardNumber = cardNumber; }
public void pay(int amount) {
System.out.println("Paid $" + amount + " using Credit Card: " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) { this.email = email; }
public void pay(int amount) {
System.out.println("Paid $" + amount + " using PayPal: " + email);
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Usage
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9999"));
cart.checkout(100);
cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout(200);
}
}
✅ Output:
Paid $100 using Credit Card: 1234-5678-9999
Paid $200 using PayPal: user@example.com
Advantages:
- Easy to add new strategies.
- Eliminates conditionals (if/else).
Disadvantages:
- Client must know available strategies.
J. Template Method Pattern
Definition:
Defines the skeleton of an algorithm in a base class, while subclasses provide specific implementations for some steps.
When to Use:
- When multiple classes share the same workflow but differ in steps.
- To enforce a standard process.
Real-World Example:
- Cooking recipe → Steps (prepare, cook, serve) are fixed, but implementation (pasta vs. biryani) differs.
Java Example:
// Abstract Class
abstract class DataProcessor {
public final void process() {
readData();
processData();
saveData();
}
abstract void readData();
abstract void processData();
public void saveData() {
System.out.println("Data saved successfully.");
}
}
// Concrete Class 1
class CSVDataProcessor extends DataProcessor {
void readData() { System.out.println("Reading CSV data..."); }
void processData() { System.out.println("Processing CSV data..."); }
}
// Concrete Class 2
class JSONDataProcessor extends DataProcessor {
void readData() { System.out.println("Reading JSON data..."); }
void processData() { System.out.println("Processing JSON data..."); }
}
// Usage
public class Main {
public static void main(String[] args) {
DataProcessor csv = new CSVDataProcessor();
csv.process();
DataProcessor json = new JSONDataProcessor();
json.process();
}
}
✅ Output:
Reading CSV data...
Processing CSV data...
Data saved successfully.
Reading JSON data...
Processing JSON data...
Data saved successfully.
Advantages:
- Promotes code reuse.
- Enforces consistent process.
Disadvantages:
- Harder to modify skeleton if subclasses need flexibility.
K. Visitor Pattern
Definition:
Separates an algorithm from the object structure it operates on, allowing new operations to be added without modifying classes.
When to Use:
- When you need to perform operations on objects of different types.
- To avoid polluting classes with unrelated operations.
Real-World Example:
- Tax calculation → Different tax rules for different product categories.
Java Example:
// Element
interface Item {
void accept(Visitor visitor);
}
// Concrete Elements
class Book implements Item {
private double price;
public Book(double price) { this.price = price; }
public double getPrice() { return price; }
public void accept(Visitor visitor) { visitor.visit(this); }
}
class Fruit implements Item {
private double weight;
public Fruit(double weight) { this.weight = weight; }
public double getWeight() { return weight; }
public void accept(Visitor visitor) { visitor.visit(this); }
}
// Visitor
interface Visitor {
void visit(Book book);
void visit(Fruit fruit);
}
// Concrete Visitor
class TaxVisitor implements Visitor {
public void visit(Book book) {
System.out.println("Tax on Book: $" + (book.getPrice() * 0.1));
}
public void visit(Fruit fruit) {
System.out.println("Tax on Fruit: $" + (fruit.getWeight() * 0.05));
}
}
// Usage
public class Main {
public static void main(String[] args) {
Item book = new Book(50);
Item fruit = new Fruit(10);
Visitor taxVisitor = new TaxVisitor();
book.accept(taxVisitor);
fruit.accept(taxVisitor);
}
}
✅ Output:
Tax on Book: $5.0
Tax on Fruit: $0.5
Advantages:
- Add new operations without modifying object structure.
- Clean separation of concerns.
Disadvantages:
- Hard to add new element types (need to update all visitors).
4. Real-World Applications of Design Patterns in Java
Design patterns are not just theory — they are heavily used in real-world frameworks, libraries, and enterprise projects.
If you’ve ever used Spring, Hibernate, or even Java’s Core Libraries, you’ve already worked with design patterns (sometimes without realizing it!).
A. How Design Patterns Are Used in Spring Framework
The Spring Framework is one of the most popular Java frameworks, and it relies heavily on design patterns:
- Singleton Pattern → Spring beans by default are singletons.
- Factory Pattern → BeanFactory and ApplicationContext are factories for creating beans.
- Proxy Pattern → Used for AOP (Aspect-Oriented Programming), transactions, and security.
- Template Method Pattern → JdbcTemplate, HibernateTemplate standardize DB operations but allow customization.
- Observer Pattern → Event handling in Spring (ApplicationListener, ApplicationEventPublisher).
🔹 Example: Spring BeanFactory (Factory Pattern)
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
MyService service = context.getBean("myService", MyService.class);
Here, the ApplicationContext is acting as a factory for bean objects.
B. Design Patterns in Hibernate
Hibernate ORM (Object-Relational Mapping) also uses design patterns extensively:
- Proxy Pattern → Lazy loading of entities (getReference()).
- Factory Pattern → SessionFactory creates Session objects.
- Singleton Pattern → Only one SessionFactory is typically created per application.
- Template Method Pattern → Defines skeleton for DB operations with customization.
- Data Access Object (DAO) Pattern → Separates persistence logic from business logic.
🔹 Example: Hibernate SessionFactory (Factory + Singleton)
SessionFactory factory = new Configuration().configure().buildSessionFactory();
Session session = factory.openSession();
- SessionFactory → Singleton + Factory.
- Session → Produced by the factory to interact with DB.
C. Patterns in Java Core Libraries
Even the Java Standard Library (JDK) is full of design patterns:
- Iterator Pattern → Iterator in Collections (list.iterator()).
- Factory Method Pattern → Calendar.getInstance(), NumberFormat.getInstance().
- Decorator Pattern → BufferedReader wrapping FileReader.
- Observer Pattern → Observable and Observer (though deprecated, concept used in modern event systems).
- Builder Pattern → StringBuilder, StringBuffer.
- Prototype Pattern → clone() method in Java objects.
- Thread Pool (Flyweight) → Executors.newFixedThreadPool() reuses threads instead of creating new ones.
🔹 Example: Decorator Pattern in Java IO
Reader reader = new BufferedReader(new FileReader("data.txt"));
Here, BufferedReader adds functionality (buffering) to FileReader without modifying it → a classic Decorator Pattern.
5. When to Use Design Patterns in Java
Design patterns are powerful, but they’re not meant to be used everywhere.
They shine the most when complexity grows and you need proven, reusable solutions.
A. Identifying Common Problems
Design patterns are useful when:
- You find yourself writing duplicate code to solve similar problems.
- You want to decouple modules so that changes in one part of the system don’t break others.
- You need to communicate solutions effectively within a team. (Saying “Use a Factory” is clearer than describing the whole implementation.)
Example:
Instead of manually managing object creation across your app, use a Factory Pattern → centralizes creation and reduces duplication.
B. Reusability and Maintainability
Patterns encourage:
- Reusability → You can reuse patterns across multiple projects (e.g., Singleton for caching).
- Maintainability → Standardized approaches make debugging and scaling easier.
- Testability → Patterns like Dependency Injection (a flavor of Strategy + Factory) make unit testing simpler.
Example:
Spring uses Dependency Injection (loosely coupled design). You can easily swap an implementation (e.g., MySQL to PostgreSQL) without touching core logic.
C. Large-Scale Applications
Design patterns are most valuable in enterprise-level systems where:
- Many developers are working together.
- Business requirements change frequently.
- System needs to be scalable, extensible, and robust.
Example:
In large e-commerce apps:
- Observer Pattern → notify users about order updates.
- Proxy Pattern → caching product details.
- Facade Pattern → simplify complex payment gateway interactions.
6. When Not to Use Design Patterns in Java
While design patterns are powerful, they are not a silver bullet.
Sometimes using them can actually make things worse — especially in small or straightforward projects.
A. Overengineering & Unnecessary Complexity
One of the biggest risks is overengineering:
- Forcing a pattern into your code when a simple solution would do.
- Writing multiple layers of abstraction for a project that doesn’t need it.
Example:
- Creating a full Abstract Factory just to create one or two objects → adds confusion instead of clarity.
👉 Remember: “Design patterns are a means to an end, not the end itself.”
B. Small / Simple Projects
Patterns may be overkill when:
- The project is small (like a utility script).
- Only one developer is maintaining the code.
- Performance and scalability are not major concerns.
Example:
- For a basic CRUD app, using Singleton + Factory + DAO + Observer is unnecessary.
- A few simple classes with straightforward methods would be easier to read and maintain.
C. Premature Optimization
Many developers misuse patterns by applying them too early, without understanding actual requirements.
This leads to:
- More code than necessary.
- Reduced readability for new developers.
- Slower development cycles.
Example:
- Adding Flyweight Pattern for memory optimization when your app doesn’t have a large object graph yet.
👉 Always measure and validate whether you really need optimization before introducing patterns.
Summary
Design patterns in Java are proven, reusable solutions to common software design problems, classified broadly into Creational, Structural, and Behavioral patterns. Creational patterns like Singleton, Factory, Builder, Prototype, and Abstract Factory focus on object creation, ensuring flexibility and reusability. Structural patterns such as Adapter, Decorator, Proxy, Facade, and Composite deal with object composition, enabling simplified interfaces and dynamic behavior addition. Behavioral patterns like Observer, Strategy, Template Method, Command, Iterator, Mediator, Memento, State, and Visitor manage object interaction and communication, improving maintainability and reducing coupling. In real-world applications, frameworks like Spring use Singleton, Factory, Proxy, Template Method, and Observer extensively, while Hibernate relies on Proxy, Factory, Singleton, DAO, and Template Method. Even Java Core Libraries implement patterns such as Iterator, Decorator, Builder, Prototype, and Flyweight. Design patterns are best applied when solving recurring problems, enhancing maintainability, and scaling large applications, but should be avoided in small projects, premature optimization, or when they introduce unnecessary complexity. Best practices include recognizing anti-patterns, balancing flexibility and simplicity, and refactoring code into patterns as complexity grows. For interviews, it’s essential to know pattern definitions, differences, real-world examples, scenario-based solutions, and small coding implementations, while always justifying why a particular pattern is chosen. By understanding the intent, applicability, and trade-offs of each pattern, developers can write cleaner, scalable, and maintainable Java code, making design patterns a powerful tool in any Java developer’s toolkit.