Design Patterns Java Tutorial: Learn Patterns from Scratch (2026)
Design patterns are reusable solutions to common software design problems. They are not templates to copy but conceptual tools that improve communication and architecture decisions. After applying patterns across dozens of Java projects, I have found that mastering a handful — rather than memorizing all 23 GoF patterns — yields the most practical value in everyday development.
This tutorial covers the most frequently used patterns in Java: Singleton, Factory, Builder, Strategy, Observer, and Decorator. Each includes a realistic use case and implementation that you can adapt to your own projects.
Singleton Pattern
Singleton ensures a class has only one instance. The enum approach is the most concise and serialization-safe — it inherently prevents reflection-based attacks. Use singletons for stateless utilities, configuration objects, or connection pools. Avoid singletons for managing mutable state in multi-threaded contexts without careful synchronization.
public enum DatabaseConnectionPool {
INSTANCE;
private final HikariDataSource dataSource;
DatabaseConnectionPool() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(System.getenv("DB_URL"));
dataSource = new HikariDataSource(config);
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
// Usage: DatabaseConnectionPool.INSTANCE.getConnection()
Factory Method Pattern
Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate. Spring's ApplicationContext is essentially a factory. Use factories when object creation involves logic (caching, pooling, conditional selection) that should not be in the constructor. The switch expression in Java 17+ makes factory implementations concise.
public interface PaymentGateway {
PaymentResult charge(PaymentRequest request);
}
public class StripeGateway implements PaymentGateway { ... }
public class PayPalGateway implements PaymentGateway { ... }
public class PaymentGatewayFactory {
public static PaymentGateway create(String provider) {
return switch (provider) {
case "stripe" -> new StripeGateway();
case "paypal" -> new PayPalGateway();
default -> throw new IllegalArgumentException("Unknown: " + provider);
};
}
}
// Usage: PaymentGatewayFactory.create("stripe")
Builder Pattern
Builder constructs complex objects step by step, avoiding telescoping constructors. Use for objects with many optional parameters or when construction requires validation. Lombok's @Builder generates builders automatically, but custom builders give you control over validation logic and default values that Lombok cannot express.
public class Order {
private final String customerId;
private final List- items;
private final String couponCode;
private final String shippingMethod;
private Order(Builder builder) {
this.customerId = builder.customerId;
this.items = Collections.unmodifiableList(builder.items);
this.couponCode = builder.couponCode;
this.shippingMethod = builder.shippingMethod;
}
public static class Builder {
private String customerId;
private List
- items = new ArrayList<>();
private String couponCode;
private String shippingMethod = "standard";
public Builder customerId(String val) { customerId = val; return this; }
public Builder addItem(Item val) { items.add(val); return this; }
public Order build() {
Objects.requireNonNull(customerId, "customerId required");
return new Order(this);
}
}
}
Strategy Pattern
Strategy defines a family of interchangeable algorithms. The pattern replaces conditional logic with composition — instead of if-else chains, inject the appropriate strategy. Spring's dependency injection makes strategies particularly elegant: each strategy is a @Component implementing a common interface.
@Service
public class ShippingService {
private final Map strategies;
public ShippingService(List strategies) {
this.strategies = strategies.stream()
.collect(Collectors.toMap(
s -> s.getClass().getSimpleName().replace("Shipping", "").toLowerCase(),
s -> s));
}
public BigDecimal calculateCost(Order order, String method) {
return strategies.get(method).calculate(order);
}
}
@Component
public class StandardShipping implements ShippingCostStrategy {
public BigDecimal calculate(Order order) {
return BigDecimal.valueOf(5.99);
}
}
Observer Pattern
Observer defines a one-to-many dependency where state changes in one object notify all dependents. In Spring, @EventListener on methods provides a clean annotation-driven observer pattern. The framework handles subscriber registration, async execution, and error handling — letting you focus on business logic.
// Custom observer
public class OrderSubject {
private final List observers = new ArrayList<>();
public void addObserver(OrderObserver o) { observers.add(o); }
public void placeOrder(Order order) {
observers.forEach(o -> o.onOrderPlaced(order));
}
}
// Spring event-driven observer
@Component
public class OrderEventListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
inventoryService.reserveStock(event.getItems());
notificationService.sendConfirmation(event.getOrderId());
}
}
Decorator Pattern
Decorator adds behavior to objects dynamically without altering their structure. Java I/O streams use this pattern extensively — BufferedInputStream wraps FileInputStream to add buffering. Use decorators for cross-cutting behavior that can be layered, like encryption, compression, or logging.
public interface Notifier {
void send(String message);
}
public class EmailNotifier implements Notifier {
public void send(String message) {
System.out.println("Email: " + message);
}
}
public abstract class NotifierDecorator implements Notifier {
protected final Notifier wrapped;
public NotifierDecorator(Notifier wrapped) { this.wrapped = wrapped; }
}
public class SlackNotifier extends NotifierDecorator {
public SlackNotifier(Notifier wrapped) { super(wrapped); }
@Override
public void send(String message) {
wrapped.send(message);
System.out.println("Slack: " + message);
}
}
// Usage: new SlackNotifier(new EmailNotifier()).send("Order shipped");
Frequently Asked Questions
Are design patterns still relevant with modern frameworks?
Yes — frameworks do not eliminate patterns; they implement them. Spring is built on Factory (bean creation), Template Method (JdbcTemplate), Proxy (AOP), and Observer (event handling). Understanding patterns helps you use frameworks more effectively.
What is the difference between Strategy and State patterns?
Strategy lets the client choose an algorithm. State changes behavior when the object's internal state changes — the context manages state transitions. Both use composition, but the intent differs.
How do I avoid overusing patterns?
Patterns solve specific problems. If a simple if-else or a straightforward class works, use it. Premature pattern application adds complexity. Apply patterns when you see the problem they solve emerging naturally.
What is the most used pattern in enterprise Java?
Dependency Injection (Spring's core) is the most pervasive pattern. Proxy (for AOP), Template Method (JdbcTemplate), and Factory (bean creation) follow closely in frequency of use.
Originally published on Ayodhyyya. Last updated June 1, 2026.