What Problems Do Design Patterns Actually Solve?
When design patterns are first introduced, they often appear as a list of names to memorize.
Creational patterns
Factory Method, Abstract Factory, Builder, Prototype, Singleton
Structural patterns
Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
Behavioral patterns
Chain of Responsibility, Command, Interpreter, Iterator,
Mediator, Memento, Observer, State, Strategy,
Template Method, Visitor
For certification exams, it is important to know each pattern’s category, definition, and identifying characteristics.
Memorizing the names alone, however, makes similar patterns difficult to distinguish.
Factory Method and Abstract Factory both create objects. Adapter and Facade both place a new interface in front of existing code. Strategy and State often produce nearly identical class diagrams. Decorator and Proxy both wrap another object behind the same interface.
The differences become clearer when we stop asking only what the class structure looks like and ask a more important question:
What design problem was this pattern created to solve?
A design pattern is not a mandatory piece of code that every application should contain. It is a named, reusable approach to a problem that developers have encountered repeatedly across many systems.
Most design patterns ultimately address one broad concern:
How can a system change without forcing unrelated parts of the codebase to change with it?
The way an object is created may change. An external library may expose an interface that does not match the application’s model. A pricing policy may need to be replaced at runtime. One state change may need to trigger several independent reactions.
Patterns identify these likely points of change and place them behind explicit objects, interfaces, or collaboration structures.
A pattern is a design vocabulary, not a code template
A design pattern should not be understood as code that must be copied exactly.
Two applications using the Strategy pattern may have completely different class names, method signatures, and dependency structures. A Factory Method does not have to be named createProduct(). An Observer implementation does not always require an interface literally named Observer.
What matters is the relationship between responsibilities.
The Strategy pattern, for example, typically contains these roles:
Context
Uses a strategy
Strategy
Defines the interchangeable algorithm contract
Concrete Strategy
Implements one version of the algorithm
In a real application, those roles may appear as:
OrderService
DiscountPolicy
FixedDiscountPolicy
RateDiscountPolicy
None of the class names includes the word Strategy.
The design still follows the pattern if the discount algorithm has been extracted behind a contract and can be replaced independently of the order-processing logic.
Patterns also provide a shared language for developers.
A developer can say:
“The discount policy is implemented as a Strategy.”
That sentence immediately communicates several expectations:
- the policy is separated from the service using it
- several implementations may exist
- the service depends on an abstraction
- the algorithm can be replaced without rewriting the main workflow
A pattern is therefore not only a reuse mechanism. It is a concise way to communicate design intent.
Design patterns exist because software changes
An application may begin with simple logic.
public final class OrderService {
public long calculatePrice(
Order order,
Customer customer
) {
long price = order.totalPrice();
if (customer.isVip()) {
return price - 10_000;
}
return price;
}
}
At first, the system supports only regular customers and VIP customers.
Then new requirements arrive:
- VIP customers receive a fixed discount.
- New customers receive a percentage discount.
- Seasonal promotions use a different formula.
- Coupons introduce additional rules.
- Partner companies have separate discount policies.
- The active policy must be selected dynamically.
The method can continue growing.
if (customer.isVip()) {
// VIP discount
} else if (customer.isNew()) {
// New-customer discount
} else if (promotion.isActive()) {
// Promotional discount
} else if (coupon != null) {
// Coupon discount
}
A few conditions are not necessarily a problem.
The deeper issue is that OrderService now has more than one reason to change. It is responsible for both the order-pricing workflow and every individual discount policy.
Changing one policy risks affecting the others. Supporting policy combinations makes the condition tree even more complicated.
The problem is not the existence of if statements.
The problem is that logic that changes for different reasons has been placed in the same unit.
The order workflow may remain stable while discount policies change frequently.
Many design patterns exist to separate these different rates and reasons for change.
Three categories of design problems
The Gang of Four patterns are divided into three categories according to their primary purpose.
Creational patterns
Creational patterns address:
How should objects be created?
Object creation becomes a design problem when construction is complex, depends on runtime configuration, or exposes too many concrete classes to the rest of the system.
Creational patterns separate object usage from object construction.
Structural patterns
Structural patterns address:
How should classes and objects be connected?
They are useful when an existing interface is incompatible, optional behavior must be added without modifying the original class, or a complex subsystem should be hidden behind a simpler entry point.
Behavioral patterns
Behavioral patterns address:
How should objects collaborate and divide responsibility?
They separate algorithms, requests, state transitions, notifications, iteration, and communication rules.
A compact way to remember the categories is:
Creational patterns:
Who creates the object, and how?
Structural patterns:
How are objects connected?
Behavioral patterns:
How do objects collaborate?
Creational patterns isolate object construction
Creating an object may look like a simple use of new.
NotificationSender sender =
new EmailNotificationSender();
In a real application, however, construction may depend on:
- runtime configuration
- environment
- user selection
- external service providers
- initialization order
- required dependencies
- object families
- validation rules
- expensive setup
When every caller knows how to construct a concrete object, a change in construction logic spreads throughout the codebase.
Creational patterns collect that knowledge in a more controlled place.
Factory Method: defer the concrete creation decision
Suppose a notification service creates its sender directly.
public final class NotificationService {
public void notify(
NotificationType type,
Message message
) {
NotificationSender sender;
if (type == NotificationType.EMAIL) {
sender = new EmailSender();
} else if (type == NotificationType.SMS) {
sender = new SmsSender();
} else {
throw new IllegalArgumentException(
"Unsupported notification type."
);
}
sender.send(message);
}
}
NotificationService is responsible for two separate concerns:
- sending the message
- deciding which concrete sender to instantiate
Adding another sender requires modifying the service.
Factory Method moves the concrete creation decision to a subclass or another overridable creation point.
public abstract class NotificationService {
public final void notify(
Message message
) {
NotificationSender sender =
createSender();
sender.send(message);
}
protected abstract NotificationSender
createSender();
}
An email-specific service creates an email sender.
public final class EmailNotificationService
extends NotificationService {
@Override
protected NotificationSender
createSender() {
return new EmailSender();
}
}
An SMS-specific service creates an SMS sender.
public final class SmsNotificationService
extends NotificationService {
@Override
protected NotificationSender
createSender() {
return new SmsSender();
}
}
The main point is not merely that object construction happens inside a method.
Factory Method allows the stable workflow to use a product while delegating the choice of concrete product to a subclass or specialized creator.
Common clues in exam questions include:
- object creation is delegated to subclasses
- a creation interface is defined
- subclasses decide which class to instantiate
- the pattern acts like a virtual constructor
Abstract Factory: create consistent families of objects
Factory Method often focuses on a creation point for one product type.
Abstract Factory focuses on families of related products that should be created and used together.
Suppose an application supports light and dark user-interface themes.
Each theme needs a matching button and text field.
public interface Button {
void render();
}
public interface TextField {
void render();
}
An abstract factory defines how to create the related components.
public interface UiFactory {
Button createButton();
TextField createTextField();
}
The light-theme factory creates only light-theme components.
public final class LightUiFactory
implements UiFactory {
@Override
public Button createButton() {
return new LightButton();
}
@Override
public TextField createTextField() {
return new LightTextField();
}
}
The dark-theme factory creates the corresponding dark components.
public final class DarkUiFactory
implements UiFactory {
@Override
public Button createButton() {
return new DarkButton();
}
@Override
public TextField createTextField() {
return new DarkTextField();
}
}
The client depends only on the factory abstraction.
public final class SettingsScreen {
private final Button button;
private final TextField textField;
public SettingsScreen(
UiFactory factory
) {
this.button =
factory.createButton();
this.textField =
factory.createTextField();
}
}
Replacing the factory replaces the entire product family while preserving a valid combination.
Abstract Factory solves the following problem:
How can a system create a consistent set of related objects without depending on their concrete classes?
Typical exam clues include:
- creates families of related or dependent objects
- avoids specifying concrete product classes
- replaces an entire family of products
- ensures related products are used together
Builder: make complex construction readable and controlled
A constructor becomes difficult to use when it has many parameters.
User user = new User(
"isaac",
"Isaac Lee",
"isaac@example.com",
"010-1234-5678",
true,
false,
3,
"ko-KR"
);
The caller cannot easily tell what each argument means.
If several parameters share the same type, placing them in the wrong order may still compile.
A Builder gives each construction step a name.
User user = User.builder()
.username("isaac")
.displayName("Isaac Lee")
.email("isaac@example.com")
.phoneNumber("010-1234-5678")
.emailEnabled(true)
.smsEnabled(false)
.maxRetryCount(3)
.locale("ko-KR")
.build();
Builder is useful when:
- an object has many optional parameters
- construction requires validation
- construction should be readable at the call site
- intermediate construction steps should remain hidden
- the same process may produce different representations
- the object should be immutable after construction
Builder does not exist merely because a constructor is long. It separates the process of assembling a complex object from the final representation.
For a small value object with two or three obvious fields, a Builder may add more ceremony than value.
Prototype: create objects by copying an existing instance
Sometimes creating an object from the beginning is expensive or complicated.
A fully configured object may already contain:
- parsed metadata
- validated configuration
- a complex object graph
- expensive initialization results
- runtime-selected concrete behavior
Prototype creates new objects by copying an existing instance.
It is useful when:
- initialization cost is high
- the concrete type is known only at runtime
- many objects share a similar initial configuration
- direct construction should be avoided
- a configured template should be duplicated
Copy semantics must be considered carefully.
A shallow copy may duplicate only the outer object while sharing mutable nested objects. Modifying a nested collection in the copy could then affect the original.
A deep copy duplicates the nested mutable state as well.
Java provides Cloneable and clone(), but their semantics and checked exception handling are often considered awkward. Modern code may instead use:
- copy constructors
- explicit copy() methods
- factory methods
- serialization-based duplication in specialized cases
The pattern’s purpose is more important than Java’s specific cloning mechanism.
Singleton: restrict a class to one instance
Singleton ensures that a class has one instance and provides access to it.
public final class ApplicationRegistry {
private static final ApplicationRegistry
INSTANCE =
new ApplicationRegistry();
private ApplicationRegistry() {
}
public static ApplicationRegistry
getInstance() {
return INSTANCE;
}
}
Common identifying characteristics are:
- the constructor is not publicly accessible
- one instance is created
- a global access point is provided
The purpose of Singleton is:
Limit the number of instances.
It should not be reduced to “a convenient way to access something from anywhere.”
A mutable, globally accessible Singleton can effectively become global state.
That creates problems such as:
- hidden dependencies
- state leaking between tests
- synchronization requirements
- difficult implementation replacement
- unclear lifecycle ownership
- unpredictable initialization order
A default-scoped Spring Bean is also commonly shared as one instance within an application context, but it is not identical to the classic Singleton pattern. The container, rather than the class itself, controls creation, wiring, and lifecycle.
Structural patterns organize object relationships
A system often contains code that already performs the required work but cannot be used directly.
Common problems include:
- an external library exposes an incompatible interface
- optional behavior must be attached without modifying the original class
- a complicated subsystem should have one simple entry point
- access to an expensive or sensitive object must be controlled
- a tree should treat leaves and groups uniformly
Structural patterns focus on how objects are connected and presented.
Adapter: translate one interface into another
Suppose an application defines its own payment contract.
public interface PaymentGateway {
PaymentResult pay(
PaymentRequest request
);
}
A legacy payment library exposes a different API.
public final class LegacyPaymentClient {
public LegacyResponse requestPayment(
String merchantId,
long amount
) {
return new LegacyResponse();
}
}
The application should not be rewritten around the legacy client’s interface.
An Adapter translates between the two models.
public final class LegacyPaymentAdapter
implements PaymentGateway {
private final LegacyPaymentClient client;
private final String merchantId;
public LegacyPaymentAdapter(
LegacyPaymentClient client,
String merchantId
) {
this.client = client;
this.merchantId = merchantId;
}
@Override
public PaymentResult pay(
PaymentRequest request
) {
LegacyResponse response =
client.requestPayment(
merchantId,
request.amount()
);
return convert(response);
}
private PaymentResult convert(
LegacyResponse response
) {
return PaymentResult.success();
}
}
An Adapter may translate:
- method names
- argument formats
- return values
- exception types
- data models
- invocation sequences
It solves this problem:
How can an existing class be used when its interface does not match the interface expected by the application?
Common exam clues include:
- converts an incompatible interface
- allows existing classes to work together
- reuses a class without modifying it
- is also described as a wrapper
Bridge: separate an abstraction from its implementation
Bridge is useful when two dimensions of a design should vary independently.
Suppose notification types and delivery platforms can both expand.
Notification types
Alert
Reminder
Report
Delivery platforms
Email
SMS
Messenger
Creating a subclass for every combination can produce a growing hierarchy.
EmailAlert
SmsAlert
MessengerAlert
EmailReminder
SmsReminder
MessengerReminder
...
Bridge separates the high-level abstraction from the implementation mechanism.
public interface MessageChannel {
void send(String message);
}
public abstract class Notification {
protected final MessageChannel channel;
protected Notification(
MessageChannel channel
) {
this.channel = channel;
}
public abstract void notifyUser(
String message
);
}
A concrete abstraction delegates delivery to the channel.
public final class AlertNotification
extends Notification {
public AlertNotification(
MessageChannel channel
) {
super(channel);
}
@Override
public void notifyUser(
String message
) {
channel.send(
"[ALERT] " + message
);
}
}
The notification hierarchy and the channel hierarchy can now evolve independently.
Bridge solves:
How can an abstraction and its implementation be extended independently instead of multiplying subclasses for every combination?
Facade: provide one simple entry point to a complex subsystem
Completing an order may require several subsystems.
reserve inventory
→ authorize payment
→ save the order
→ request shipping
→ send a notification
If every caller must understand all services, invocation order, and error-handling rules, the subsystem becomes difficult to use.
A Facade provides a higher-level operation.
public final class OrderFacade {
private final InventoryService inventory;
private final PaymentService payment;
private final ShippingService shipping;
private final NotificationService notification;
public OrderResult placeOrder(
OrderCommand command
) {
inventory.reserve(
command.productId(),
command.quantity()
);
PaymentResult paymentResult =
payment.pay(
command.payment()
);
ShippingResult shippingResult =
shipping.request(
command.shippingAddress()
);
notification.sendOrderCompleted(
command.customerId()
);
return new OrderResult(
paymentResult,
shippingResult
);
}
}
The caller uses a single operation.
OrderResult result =
orderFacade.placeOrder(command);
Facade does not primarily convert an incompatible interface.
It hides complexity behind a simpler interface.
Adapter
Converts one interface into another expected interface.
Facade
Provides a simpler interface to a complex subsystem.
Decorator: add behavior without subclass explosion
Suppose a sender may need optional features such as:
- encryption
- compression
- timing
- logging
- metrics
Creating a subclass for every combination quickly becomes impractical.
EncryptedSender
CompressedSender
TimedSender
EncryptedCompressedSender
TimedEncryptedSender
TimedCompressedEncryptedSender
Decorator implements the same interface as the original object and delegates to another object of that interface.
public interface MessageSender {
void send(Message message);
}
The core sender performs the main operation.
public final class EmailSender
implements MessageSender {
@Override
public void send(
Message message
) {
System.out.println(
"Sending email."
);
}
}
An encryption decorator adds behavior before delegation.
public final class EncryptedSender
implements MessageSender {
private final MessageSender delegate;
public EncryptedSender(
MessageSender delegate
) {
this.delegate = delegate;
}
@Override
public void send(
Message message
) {
Message encrypted =
encrypt(message);
delegate.send(encrypted);
}
private Message encrypt(
Message message
) {
return message;
}
}
A timing decorator wraps the call.
public final class TimedSender
implements MessageSender {
private final MessageSender delegate;
public TimedSender(
MessageSender delegate
) {
this.delegate = delegate;
}
@Override
public void send(
Message message
) {
long startedAt =
System.nanoTime();
try {
delegate.send(message);
} finally {
long elapsed =
System.nanoTime()
- startedAt;
System.out.println(
"Elapsed: " + elapsed
);
}
}
}
Decorators can be composed dynamically.
MessageSender sender =
new TimedSender(
new EncryptedSender(
new EmailSender()
)
);
Decorator solves:
How can responsibilities be added to individual objects dynamically without modifying the original class or creating a subclass for every combination?
Proxy: control access to another object
Proxy also implements the same interface as the real object and delegates to it.
Its purpose, however, is usually different from Decorator.
Decorator focuses on adding composable responsibilities.
Proxy focuses on controlling or managing access to the target.
public interface DocumentService {
Document load(Long documentId);
}
A security proxy can enforce authorization.
public final class SecuredDocumentService
implements DocumentService {
private final DocumentService target;
private final AccessPolicy accessPolicy;
public SecuredDocumentService(
DocumentService target,
AccessPolicy accessPolicy
) {
this.target = target;
this.accessPolicy = accessPolicy;
}
@Override
public Document load(
Long documentId
) {
accessPolicy.checkReadPermission(
documentId
);
return target.load(documentId);
}
}
Proxy variants may handle:
- authorization
- lazy initialization
- remote access
- caching
- transactions
- rate limiting
- monitoring
Spring AOP proxies, including those used to apply transactional and security behavior, are closely related to this pattern.
Common exam clues include:
- a surrogate or representative object
- controlled access to the real object
- delayed creation
- remote object representation
- access management
Composite: treat individual objects and groups uniformly
A file system contains files and directories.
A file has a size. A directory contains files and other directories.
Without a shared abstraction, callers must handle leaves and containers separately.
Composite defines a common component interface.
public interface FileSystemNode {
long size();
}
A file is a leaf.
public final class FileNode
implements FileSystemNode {
private final long size;
public FileNode(long size) {
this.size = size;
}
@Override
public long size() {
return size;
}
}
A directory is a composite.
public final class DirectoryNode
implements FileSystemNode {
private final List<FileSystemNode>
children;
public DirectoryNode(
List<FileSystemNode> children
) {
this.children =
List.copyOf(children);
}
@Override
public long size() {
return children.stream()
.mapToLong(
FileSystemNode::size
)
.sum();
}
}
The caller can invoke size() without knowing whether the node is a file or a directory.
Composite solves:
How can leaf objects and groups of objects be treated through the same interface?
Its strongest clue is a recursive tree structure containing parts and wholes.
Flyweight: share intrinsic state across many small objects
An application may need a very large number of similar objects.
Examples include:
- characters in a text editor
- map markers
- particles in a game
- graphical icons
- repeated formatting definitions
If every object stores the same heavy data independently, memory consumption becomes excessive.
Flyweight separates state into two categories:
- intrinsic state, which can be shared
- extrinsic state, which depends on the current context
A character’s font definition may be shared, while its position in a document remains external.
Flyweight solves:
How can a system support many fine-grained objects efficiently by sharing common immutable state?
Exam clues include:
- object sharing
- reduced memory usage
- a large number of similar objects
- separation of intrinsic and extrinsic state
Behavioral patterns organize collaboration
Behavioral patterns focus on how responsibilities move between objects.
They address questions such as:
- Which algorithm should be used?
- Who should receive a request?
- How should state changes affect behavior?
- How should one object notify many others?
- How can a request be stored and executed later?
- How should a collection be traversed?
Strategy: make algorithms interchangeable
Return to the discount example.
Extract the discount algorithm behind an interface.
public interface DiscountPolicy {
long discount(
Order order,
Customer customer
);
}
A fixed VIP policy returns a fixed amount.
public final class VipDiscountPolicy
implements DiscountPolicy {
@Override
public long discount(
Order order,
Customer customer
) {
return 10_000;
}
}
A percentage policy uses a configurable rate.
public final class RateDiscountPolicy
implements DiscountPolicy {
private final int rate;
public RateDiscountPolicy(int rate) {
this.rate = rate;
}
@Override
public long discount(
Order order,
Customer customer
) {
return order.totalPrice()
* rate
/ 100;
}
}
The service depends only on the contract.
public final class OrderService {
private final DiscountPolicy
discountPolicy;
public OrderService(
DiscountPolicy discountPolicy
) {
this.discountPolicy =
discountPolicy;
}
public long calculatePrice(
Order order,
Customer customer
) {
long discount =
discountPolicy.discount(
order,
customer
);
return order.totalPrice()
- discount;
}
}
The policy can be replaced without modifying the service.
DiscountPolicy policy =
new RateDiscountPolicy(10);
OrderService service =
new OrderService(policy);
Strategy solves:
How can several algorithms serving the same purpose be encapsulated and replaced independently of the code that uses them?
Common exam clues include:
- defines a family of algorithms
- encapsulates each algorithm
- makes algorithms interchangeable
- allows the algorithm to vary independently from its client
State: change behavior as internal state changes
An order behaves differently depending on its state.
if (status == CREATED) {
// Payment is allowed
} else if (status == PAID) {
// Shipping can begin
} else if (status == SHIPPED) {
// Delivery can be completed
} else if (status == CANCELED) {
// Most operations are rejected
}
When state-dependent conditions appear across many methods, the class becomes difficult to maintain.
State moves each state’s behavior into its own object.
public interface OrderState {
void pay(OrderContext order);
void cancel(OrderContext order);
}
The created state allows payment and cancellation.
public final class CreatedState
implements OrderState {
@Override
public void pay(
OrderContext order
) {
order.changeState(
new PaidState()
);
}
@Override
public void cancel(
OrderContext order
) {
order.changeState(
new CanceledState()
);
}
}
The paid state rejects a second payment.
public final class PaidState
implements OrderState {
@Override
public void pay(
OrderContext order
) {
throw new IllegalStateException(
"The order has already been paid."
);
}
@Override
public void cancel(
OrderContext order
) {
order.changeState(
new CanceledState()
);
}
}
Strategy and State often look structurally similar.
Their intentions differ.
Strategy
Selects how an operation should be performed.
State
Changes behavior according to the object's current state.
A Strategy is often supplied by an external configuration or caller.
A State object commonly changes as the context moves through its lifecycle.
Observer: notify many dependents about one change
Completing an order may trigger several independent reactions:
- send an email
- update inventory
- record analytics
- grant reward points
- begin shipping
If OrderService directly knows every dependent service, its coupling grows continuously.
public void completeOrder(
Order order
) {
order.complete();
emailService.send(order);
inventoryService.confirm(order);
analyticsService.record(order);
pointService.add(order);
shippingService.start(order);
}
Observer defines a subscription contract.
public interface OrderCompletedListener {
void onOrderCompleted(
OrderCompletedEvent event
);
}
Each reaction becomes an independent listener.
public final class EmailListener
implements OrderCompletedListener {
@Override
public void onOrderCompleted(
OrderCompletedEvent event
) {
// Send email
}
}
public final class AnalyticsListener
implements OrderCompletedListener {
@Override
public void onOrderCompleted(
OrderCompletedEvent event
) {
// Record analytics
}
}
The publisher announces the event without depending on every implementation detail.
Observer solves:
How can one object notify several dependent objects without knowing the details of every reaction?
Observer is related to event-driven programming, but it does not automatically imply asynchronous processing.
Listeners may be called synchronously in the same process, or notifications may be delivered through an asynchronous messaging system.
The core idea is the one-to-many dependency and reduced coupling between publisher and subscribers.
Template Method: fix the workflow, vary selected steps
Several import jobs may share the same sequence:
validate
→ parse
→ save
→ create result
The sequence should remain stable, but the parsing logic differs by file type.
Template Method places the algorithm skeleton in a parent class.
public abstract class ImportJob {
public final ImportResult execute(
ImportFile file
) {
validate(file);
List<Record> records =
parse(file);
int savedCount =
save(records);
return new ImportResult(
savedCount
);
}
protected void validate(
ImportFile file
) {
if (file == null
|| file.isEmpty()) {
throw new IllegalArgumentException(
"The file must not be empty."
);
}
}
protected abstract List<Record>
parse(ImportFile file);
protected abstract int save(
List<Record> records
);
}
A subclass implements only the variable steps.
public final class CsvImportJob
extends ImportJob {
@Override
protected List<Record> parse(
ImportFile file
) {
return List.of();
}
@Override
protected int save(
List<Record> records
) {
return records.size();
}
}
Template Method solves:
How can an algorithm’s overall sequence remain fixed while subclasses customize selected steps?
Compared with Strategy:
Template Method
Uses inheritance.
The parent owns the algorithm skeleton.
Subclasses override selected steps.
Strategy
Uses composition.
The entire algorithm is moved into another object.
Strategies can usually be replaced more freely.
Command: turn a request into an object
An ordinary method call is usually executed immediately.
light.turnOn();
Sometimes a request must be:
- delayed
- queued
- logged
- retried
- undone
- grouped
- sent to another process
- executed by an object that does not know the requester
Command represents the request as an object.
public interface Command {
void execute();
}
public final class TurnOnLightCommand
implements Command {
private final Light light;
public TurnOnLightCommand(
Light light
) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
A queue can store and execute commands later.
public final class CommandQueue {
private final Queue<Command>
commands =
new ArrayDeque<>();
public void add(Command command) {
commands.add(command);
}
public void executeNext() {
Command command =
commands.remove();
command.execute();
}
}
Command solves:
How can a request be treated as data so that it can be stored, passed, scheduled, retried, logged, or undone?
The pattern is not merely a method wrapped in a class. Its value comes from giving a request an independent lifecycle.
Chain of Responsibility: pass a request through potential handlers
Sometimes several objects may be able to process a request, and the sender should not choose the exact handler.
Consider a support request moving through:
automatic FAQ response
→ general support agent
→ technical specialist
→ administrator
Each handler either processes the request or forwards it.
public abstract class SupportHandler {
private SupportHandler next;
public SupportHandler setNext(
SupportHandler next
) {
this.next = next;
return next;
}
public final void handle(
SupportRequest request
) {
if (canHandle(request)) {
process(request);
return;
}
if (next != null) {
next.handle(request);
return;
}
throw new IllegalStateException(
"No handler accepted the request."
);
}
protected abstract boolean canHandle(
SupportRequest request
);
protected abstract void process(
SupportRequest request
);
}
Chain of Responsibility solves:
How can a request be passed through a sequence of possible handlers without coupling the sender to one specific receiver?
Filter chains, middleware pipelines, authentication steps, and validation chains often use related structures.
Iterator: traverse a collection without exposing its structure
Arrays, linked lists, trees, and custom collections store elements differently.
If callers need to understand every internal representation, traversal logic becomes tightly coupled to storage details.
Iterator provides sequential access without exposing the representation.
Java’s enhanced for loop is built on Iterable and Iterator.
for (Order order : orders) {
process(order);
}
The caller does not need to know whether orders is backed by an array, linked structure, or another collection type.
Iterator solves:
How can elements be traversed without exposing the collection’s internal representation?
Mediator: centralize complicated object communication
A user interface may contain components with many dependencies:
- a checkbox enables an input
- changing the input updates a button
- pressing the button changes a list
- the list selection updates a label
If every component references every other component, the communication graph becomes difficult to understand.
Mediator places the interaction rules in a central object.
Button ─┐
Input ─┼── DialogMediator
List ─┤
Label ─┘
Each component communicates with the mediator rather than directly coordinating with every other component.
Mediator solves:
How can complex many-to-many communication be centralized so that participating objects remain less coupled?
The mediator itself can become too large if every system rule is placed inside it. The pattern reduces distributed communication complexity, but it does not eliminate the need for responsible decomposition.
Memento: save and restore state without exposing internals
An editor may need undo support.
Directly exposing all internal fields so another object can save them would break encapsulation.
Memento allows the originator to create a snapshot of its own state. A caretaker stores the snapshot without needing to understand its contents.
It is useful for:
- undo and redo
- checkpoints
- rollback of user operations
- restoration of previous configuration
- temporary state recovery
Memento solves:
How can an object’s previous state be captured and restored without exposing its internal representation?
The trade-off is storage cost. Repeatedly saving large object graphs can consume substantial memory.
Visitor: add operations to a stable object structure
Suppose a document contains several element types:
TextElement
ImageElement
TableElement
Many operations must be performed on them:
- HTML export
- PDF export
- statistics
- accessibility checks
- search indexing
Adding every operation to every element class mixes the object structure with an expanding set of unrelated behaviors.
Visitor separates operations from the elements.
It is especially useful when:
- the set of element types is relatively stable
- new operations are added frequently
- operations need type-specific behavior
- double dispatch is useful
Visitor makes new operations easier to add.
Its trade-off is that adding a new element type may require updating every existing visitor.
Visitor solves:
How can new operations be added to a stable object structure without placing every operation inside the element classes?
Interpreter: represent and evaluate a language grammar
Interpreter defines a representation for a simple language or grammar and provides a way to interpret expressions in that language.
It may be used for:
- simple rule languages
- search expressions
- arithmetic expressions
- configuration grammars
- access-control rules
For example, an expression tree might contain:
AndExpression
OrExpression
VariableExpression
ConstantExpression
Each expression interprets itself within a context.
Interpreter solves:
How can the grammar and evaluation rules of a small language be represented as objects?
It is usually suitable for relatively simple grammars. For complex languages, parser generators and dedicated compiler techniques are generally more appropriate.
Similar patterns should be distinguished by intent
Certification questions often describe a situation and ask for the matching pattern.
Because several patterns have similar class structures, their intent is the more reliable clue.
Factory Method vs. Abstract Factory
Factory Method
- focuses on a creation point
- often delegates creation to a subclass
- decides which concrete product to instantiate
- commonly relies on inheritance
Abstract Factory
- creates several related product types
- keeps product combinations consistent
- replaces a whole product family
- often contains multiple creation methods
Factory Method
Which sender should this creator produce?
Abstract Factory
Which matching button, input, and menu family should be produced?
Adapter vs. Facade
Adapter
- an existing interface is incompatible
- translates it into an expected interface
- focuses on compatibility
Facade
- a subsystem is complicated
- provides a simpler entry point
- focuses on ease of use and reduced coupling
Adapter
Makes an incompatible plug fit the socket.
Facade
Provides one control panel for a complicated machine.
Decorator vs. Proxy
Both may:
- implement the same interface as the target
- hold a reference to another object
- delegate method calls
Their intentions differ.
Decorator
- adds responsibilities
- supports flexible composition
- often wraps several decorators around one component
Proxy
- controls access
- manages target creation or invocation
- may perform authorization, remote access, caching, or lazy loading
Strategy vs. State
Both often contain a context interface and multiple implementations.
Strategy
- represents interchangeable algorithms
- is often selected by a caller or configuration
- answers “How should this be done?”
State
- represents lifecycle states
- changes as the context transitions
- answers “How should this object behave in its current state?”
Template Method vs. Strategy
Both isolate varying algorithms.
Template Method
- uses inheritance
- fixes the algorithm skeleton in a superclass
- allows subclasses to override individual steps
Strategy
- uses composition
- moves the algorithm into another object
- makes replacement and combination easier
Observer vs. Mediator
Observer
- one publisher notifies many subscribers
- models one-to-many dependency
- focuses on distributing change notifications
Mediator
- coordinates communication among several participants
- simplifies many-to-many interaction
- focuses on centralizing collaboration rules
Design patterns and SOLID principles
Design patterns are closely connected to object-oriented design principles.
Patterns are not automatic proof that SOLID has been followed, but they often provide concrete structures through which those principles can be applied.
Single Responsibility Principle
A class should have one primary reason to change.
- Strategy separates policy from the service using it.
- Factory separates construction from usage.
- Observer separates follow-up reactions from the publisher.
- Adapter separates external integration details from business logic.
Open-Closed Principle
Software entities should be open for extension but closed to unnecessary modification.
- a new Strategy implementation can be added
- a new Decorator can be composed
- a new Observer can subscribe
- a new Command type can be introduced
Adding an interface does not automatically satisfy the Open-Closed Principle.
The real question is whether a likely new requirement can be introduced without repeatedly modifying stable code.
Liskov Substitution Principle
A subtype must preserve the expectations of the abstraction it implements.
Every DiscountPolicy should behave according to the policy contract.
An implementation that throws UnsupportedOperationException for an operation promised by the interface may violate substitutability.
Patterns rely heavily on interchangeable implementations, so behavioral compatibility matters as much as matching method signatures.
Interface Segregation Principle
Clients should not depend on operations they do not use.
Small, cohesive interfaces such as:
DiscountPolicy
Command
OrderCompletedListener
MessageChannel
are often more useful than one large interface containing unrelated operations.
Dependency Inversion Principle
High-level policy should depend on abstractions rather than concrete infrastructure.
private final PaymentGateway gateway;
The application service depends on an application-defined contract instead of a vendor-specific SDK.
Adapter, Strategy, Abstract Factory, and Bridge commonly support this kind of dependency direction.
Sometimes the best pattern is no pattern
After learning design patterns, it is tempting to apply them everywhere.
A simple object creation receives a Factory. A single implementation receives a Strategy interface. A direct method call becomes an event-based Observer system.
Patterns do not remove complexity for free.
They exchange one kind of complexity for another.
Strategy may reduce a large conditional, but it introduces:
- an interface
- implementation classes
- object-selection logic
- dependency wiring
- additional files
- indirect execution flow
When behavior is simple and unlikely to vary, a direct method may be clearer.
public long calculateTax(
long amount
) {
return amount * 10 / 100;
}
If the tax rule is fixed and the application does not support multiple policies, introducing TaxStrategy, DefaultTaxStrategy, and TaxStrategyFactory may be premature.
Useful questions include:
- Is there a real recurring design problem?
- Which code changes for different reasons?
- Are new implementations likely?
- Is a condition block mixing distinct responsibilities?
- Will the pattern make the caller simpler?
- Is the flexibility worth the additional classes?
- Can the team understand and maintain the structure?
- Does the pattern solve today’s problem or only an imagined future problem?
Patterns should respond to observed pressure in the design, not to a desire to make the code appear sophisticated.
Find the change before selecting the pattern
A good pattern decision begins by identifying what changes.
The following table provides a practical starting point.
Recurring problemPattern to consider
| An algorithm or policy changes frequently | Strategy |
| Behavior changes with internal state | State |
| Object construction varies or becomes complex | Factory Method, Abstract Factory, Builder |
| An external interface does not match the internal contract | Adapter |
| Optional responsibilities should be composed | Decorator |
| Access to an object must be managed | Proxy |
| A complex subsystem needs a simpler entry point | Facade |
| One event should notify several independent objects | Observer |
| The workflow is fixed but selected steps vary | Template Method |
| Requests must be queued, logged, retried, or undone | Command |
| A request should pass through several potential handlers | Chain of Responsibility |
| Leaves and groups in a tree should be treated uniformly | Composite |
| An abstraction and implementation should vary independently | Bridge |
| Many similar objects should share immutable state | Flyweight |
This table is not a universal answer key.
The same problem may be solved differently depending on:
- runtime requirements
- team familiarity
- expected change
- framework conventions
- performance constraints
- testability
- system boundaries
The pattern name should follow the design problem, not replace the analysis.
Recognizing design patterns in exam questions
Pattern questions often contain characteristic phrases.
Creational patterns
Factory Method
delegates creation to subclasses
defines an interface for creating an object
subclasses decide which class to instantiate
virtual constructor
Abstract Factory
creates families of related objects
creates dependent objects without concrete classes
replaces an entire product family
Builder
separates construction from representation
constructs a complex object step by step
uses the same process to produce different representations
Prototype
creates an object by copying a prototype
clones an existing instance
Singleton
restricts a class to one instance
provides a global access point
Structural patterns
Adapter
converts an incompatible interface
allows existing classes to work together
Bridge
separates abstraction from implementation
allows both sides to vary independently
Composite
treats parts and wholes uniformly
represents a tree structure
Decorator
adds responsibilities dynamically
provides flexible extension without subclassing
Facade
provides a simple interface to a complex subsystem
Flyweight
shares reusable objects
reduces memory usage for many similar objects
Proxy
uses a surrogate object
controls access to the real object
Behavioral patterns
Chain of Responsibility
passes a request through linked handlers
continues until one handler processes it
Command
encapsulates a request as an object
supports queuing, logging, and undo
Interpreter
represents a grammar
defines how language expressions are interpreted
Iterator
accesses elements sequentially
does not expose the collection's representation
Mediator
centralizes complex communication
reduces direct dependencies among objects
Memento
captures and restores state
preserves encapsulation
Observer
defines a one-to-many dependency
automatically notifies dependents of a change
State
changes behavior when internal state changes
appears to change class as state changes
Strategy
defines a family of algorithms
encapsulates and exchanges algorithms
Template Method
defines an algorithm skeleton in a superclass
allows subclasses to implement selected steps
Visitor
separates operations from an object structure
adds operations without modifying element classes
The value of a pattern appears in the next change
A pattern-based design is not always shorter at the beginning.
Strategy introduces interfaces and implementation classes. Factory adds an indirect construction path. Observer can make the execution flow less obvious because the publisher no longer calls every reaction directly.
The value of the pattern often appears when the next requirement arrives.
Can a new discount policy be added without modifying the order service?
Can a new payment provider be integrated without leaking vendor SDK types into business logic?
Can another order-completion reaction be registered without rewriting the completion workflow?
Can timing, authorization, or caching be added without changing every implementation class?
Patterns narrow the scope of such changes.
Before isolating the change
One requirement spreads across conditions and classes.
After isolating the change
The changing responsibility is concentrated behind
a specific contract and implementation boundary.
A poorly selected pattern can produce the opposite result.
The code gains unnecessary abstractions. A simple execution path requires navigating several files. The class hierarchy reflects a textbook diagram rather than a real design pressure.
That is why the reason for using a pattern matters more than the presence of the pattern itself.
A design pattern is an intent, not a name
Understanding design patterns means more than memorizing twenty-three names.
When reading code, we should be able to ask:
Which part of this design is likely to change?
Are creation and usage responsibilities mixed together?
Is business logic directly coupled to a concrete implementation?
Are conditionals combining independent policies or lifecycle states?
Have communication rules become tangled across many objects?
Could composition provide more flexible extension than inheritance?
Creational patterns isolate changes in object construction.
Structural patterns organize existing objects into more useful relationships.
Behavioral patterns separate algorithms, requests, states, traversal, and communication.
Their specific structures differ, but their direction is similar:
Find what changes and separate it from what should remain stable.
Factory separates the choice of concrete type.
Adapter separates external interface differences.
Strategy separates interchangeable algorithms.
Observer separates a publisher from its follow-up reactions.
Template Method separates a stable workflow from customizable steps.
Decorator separates core behavior from optional responsibilities.
Design patterns are therefore not primarily techniques for producing special-looking class diagrams.
They are ways to decide how far a change should travel through a system.
A good design is not one that uses patterns everywhere.
It is one that solves the current problem simply while placing real, recurring variation behind boundaries that are easy to understand and change.
The first question should not be:
Which pattern should I use?
It should be:
What design problem keeps recurring here, and which responsibilities are likely to change for different reasons?
Once that question has a clear answer, design patterns stop being a list to memorize.
They become a practical vocabulary for building software that can change without forcing the entire system to change with it.
