Software Design Patterns Every Developer Should Know
Practical examples of Singleton, Factory Method, Builder, Facade, Adapter, Decorator, Strategy, Observer, State, and Command.
Imagine you’re building an e-commerce application.
At first, things are simple. A customer places an order, makes a payment, and receives a confirmation.
Then requirements grow. You need multiple payment providers, different shipping options, order status transitions, notifications, and support for external systems.
Without a thoughtful design, every new requirement can add more conditional statements, dependencies, and duplicated logic.
Software design patterns offer reusable ways to solve these recurring design problems.
They aren’t ready-made solutions that should be applied everywhere. Think of them as blueprints you can adapt to the needs of your application.
Let’s explore ten patterns with practical Java examples.
Understanding the Three Categories
Creational patterns deal with object creation. Structural patterns deal with how components fit together. Behavioral patterns deal with how objects communicate and organize behavior.
1. Singleton Pattern
Problem: Multiple instances of a component may create inconsistent shared state.
Concept: Singleton restricts a class to one instance within a defined scope.
class AppConfig {
private AppConfig() {}private static class Holder {
static final AppConfig INSTANCE = new AppConfig();
} public static AppConfig getInstance() {
return Holder.INSTANCE;
}
}
Bad versus good: Instead of independently creating instances of a resource intended to be shared, the application accesses one managed instance.
Real-world application: Process-scoped registries or configuration accessors.
When not to use it: Avoid global mutable state and hidden dependencies. Dependency injection is often a better choice for application services.
Related concepts: Encapsulation, dependency injection, creational patterns.
2. Factory Method Pattern
Problem: Business logic becomes cluttered with object creation and conditionals for different implementations.
Concept: Specialized creators determine which concrete product to instantiate.
interface ReportGenerator {
void generate();
}class PdfReport implements ReportGenerator {
public void generate() {
System.out.println("Generating PDF");
}
}abstract class ReportCreator {
abstract ReportGenerator createReport(); void generateReport() {
createReport().generate();
}
}class PdfReportCreator extends ReportCreator {
ReportGenerator createReport() {
return new PdfReport();
}
}
Bad versus good: The workflow no longer needs to know how each concrete report generator is constructed.
Real-world application: Report generators, document exporters, plugin frameworks.
When not to use it: A simple factory function may be enough when a full creator hierarchy adds little value.
Related concepts: OCP, DIP, Builder.
3. Builder Pattern
Problem: Constructors with many optional parameters become difficult to read and maintain.
Concept: Builder allows an object to be configured step by step before it is created.
OrderRequest request = new OrderRequest.Builder()
.customerId("C101")
.address("Noida")
.paymentMethod("UPI")
.giftWrap(true)
.build();Bad versus good: Instead of a long constructor with ambiguous arguments, named builder methods make the configuration explicit.
Real-world application: API requests, application configuration, immutable objects, test fixtures.
When not to use it: For small objects with only a few straightforward fields, a constructor is often enough.
Related concepts: Encapsulation, immutability, KISS.
4. Facade Pattern
Problem: A client needs to coordinate many components to perform one operation.
Concept: Facade provides a simplified interface to a complex subsystem.
For example, a checkout facade might expose:
checkoutFacade.placeOrder(orderRequest);Internally, it coordinates inventory validation, payment processing, order persistence, and notifications.
Bad versus good: The controller no longer needs to know every internal step of checkout.
Real-world application: Checkout workflows, home automation, cloud SDKs, media processing.
When not to use it: Avoid creating a giant facade that absorbs every business rule or merely forwards every method without simplifying anything.
Related concepts: SRP, KISS, Adapter.
5. Adapter Pattern
Problem: An external service exposes an interface different from the one your application expects.
Concept: Adapter translates between the two interfaces.
interface PaymentGateway {
void pay(double amount);
}class ExternalPay {
void makePayment(int paise) {
System.out.println("Paid " + paise + " paise");
}
}class PaymentAdapter implements PaymentGateway {
private final ExternalPay provider; PaymentAdapter(ExternalPay provider) {
this.provider = provider;
} public void pay(double amount) {
int paise = (int) Math.round(amount * 100);
provider.makePayment(paise);
}
}
Bad versus good: Provider-specific method calls and conversions are isolated in the adapter rather than spread throughout business logic.
Real-world application: Payment providers, legacy APIs, external SDKs, incompatible data formats.
When not to use it: If interfaces already match, an adapter may be unnecessary. Real integrations also need appropriate error handling, currency precision, and security.
Related concepts: DIP, Strategy, Facade.
6. Decorator Pattern
Problem: Adding optional behaviors through inheritance can create many subclasses for different feature combinations.
Get Kunal Arora’s stories in your inbox
Join Medium for free to get updates from this writer.
Concept: Decorator wraps an object with another object that implements the same interface, adding behavior without changing the original implementation.
interface MessageSender {
void send(String message);
}class BasicSender implements MessageSender {
public void send(String message) {
System.out.println("Sending: " + message);
}
}class LoggingDecorator implements MessageSender {
private final MessageSender sender; LoggingDecorator(MessageSender sender) {
this.sender = sender;
} public void send(String message) {
System.out.println("Logging message");
sender.send(message);
}
}
Bad versus good: Rather than creating a subclass for every combination of logging, metrics, and other behaviors, decorators can be composed.
Real-world application: HTTP clients, middleware, logging, compression, metrics, stream processing.
When not to use it: Excessive wrapping can make execution flow hard to trace.
Related concepts: OCP, composition over inheritance, Adapter.
7. Strategy Pattern
Problem: A service accumulates conditional statements for different algorithms.
Concept: Strategy encapsulates interchangeable algorithms behind a common interface.
interface ShippingStrategy {
double calculate(double weight);
}class StandardShipping implements ShippingStrategy {
public double calculate(double weight) {
return weight * 5;
}
}class ExpressShipping implements ShippingStrategy {
public double calculate(double weight) {
return weight * 12;
}
}class ShippingService {
private final ShippingStrategy strategy; ShippingService(ShippingStrategy strategy) {
this.strategy = strategy;
} double getCost(double weight) {
return strategy.calculate(weight);
}
}
Bad versus good: A central class no longer owns every shipping algorithm. The selected strategy handles its own calculation.
Real-world application: Pricing, shipping, discount calculation, routing, authentication.
When not to use it: If there is only one stable algorithm, a simple function may be clearer.
Related concepts: OCP, DIP, composition over inheritance, State.
8. Observer Pattern
Problem: One component needs to notify multiple other components, but directly calling every consumer creates tight coupling.
Concept: Observer allows interested components to register and receive notifications when an event occurs.
interface OrderObserver {
void onOrderPlaced(String orderId);
}class EmailObserver implements OrderObserver {
public void onOrderPlaced(String orderId) {
System.out.println("Email for " + orderId);
}
}class AnalyticsObserver implements OrderObserver {
public void onOrderPlaced(String orderId) {
System.out.println("Analytics for " + orderId);
}
}
A publisher maintains observers and notifies them when an order is placed.
Bad versus good: The order service doesn’t need to know every component that reacts to an order event.
Real-world application: UI events, notifications, analytics, monitoring, event-driven applications.
When not to use it: If only one component needs to be called, a direct method call may be simpler. Distributed systems also need reliable delivery and failure-handling mechanisms beyond a basic in-memory observer list.
Related concepts: SRP, OCP, event-driven architecture.
9. State Pattern
Problem: An object’s valid operations change depending on its current state, leading to complicated conditional logic.
Concept: State represents state-specific behavior in separate objects.
interface OrderState {
void ship(Order order);
}class PaidState implements OrderState {
public void ship(Order order) {
System.out.println("Shipping order");
order.setState(new ShippedState());
}
}class ShippedState implements OrderState {
public void ship(Order order) {
throw new IllegalStateException(
"Order has already shipped"
);
}
}
An Order delegates shipping behavior to its current state. A complete implementation would also define initialization and enforce all valid transitions.
Bad versus good: Instead of repeatedly checking the order status throughout the application, state-specific behavior is organized into state objects.
Real-world application: Order lifecycles, payment workflows, approval systems, support tickets, connection management.
When not to use it: For only two simple states with minimal behavior, an enum and a small conditional may be enough.
Related concepts: Encapsulation, Strategy, state machines.
10. Command Pattern
Problem: Operations are implemented directly inside callers, making it difficult to queue, log, or undo them.
Concept: Command represents an operation as an object.
interface Command {
void execute();
void undo();
}class Light {
void turnOn() {
System.out.println("Light ON");
} void turnOff() {
System.out.println("Light OFF");
}
}class TurnOnCommand implements Command {
private final Light light; TurnOnCommand(Light light) {
this.light = light;
} public void execute() {
light.turnOn();
} public void undo() {
light.turnOff();
}
}
A button can hold a command and execute it without knowing how the underlying light works. A history manager can retain commands to support undo operations.
Bad versus good: The caller no longer needs to contain all the operation’s implementation details.
Real-world application: Undo/redo, job queues, task scheduling, menu actions, workflow systems.
When not to use it: For simple operations that never need to be stored, queued, or reversed, a direct method call is often simpler. Real undo operations may require saved previous state or compensating actions.
Related concepts: Observer, SRP, event queues, undo/redo systems.
How Do You Choose the Right Pattern?
Start with the problem rather than the pattern name.
ProblemPattern to considerNeed one instance within a defined scopeSingletonNeed specialized object creationFactory MethodNeed readable construction with many optionsBuilderNeed a simplified interface to a complex subsystemFacadeNeed to integrate an incompatible interfaceAdapterNeed to add optional behavior dynamicallyDecoratorNeed interchangeable algorithmsStrategyNeed to notify multiple listenersObserverNeed behavior to change with lifecycle stateStateNeed to represent operations as objectsCommand
Patterns can be combined when each solves a distinct problem. For example, a payment system might use Strategy to select a payment approach, Adapter to integrate external providers, and Observer to publish payment events.
Final Thoughts
Design patterns give developers a shared vocabulary for recurring engineering problems. But applying more patterns doesn’t automatically mean better architecture.
A small function may be enough. A conditional statement may be clearer than several classes. A pattern becomes valuable when its benefits outweigh its complexity.
Learn the problem first, understand the trade-offs, and then choose the pattern that makes the code easier to understand and maintain.





