| name | design-patterns |
| description | Common design patterns with Java examples (Factory, Builder, Strategy, Observer, Decorator, etc.). Use when user asks "implement pattern", "use factory", "strategy pattern", or when designing extensible components. |
| license | MIT |
Design Patterns Skill
Practical design patterns reference for Java with modern examples.
When to Use
- User asks to implement a specific pattern
- Designing extensible/flexible components
- Refactoring rigid code structures
- Code review suggests pattern usage
Quick Reference: When to Use What
| Problem | Pattern |
|---|
| Complex object construction | Builder |
| Create objects without specifying class | Factory |
| Multiple algorithms, swap at runtime | Strategy |
| Add behavior without changing class | Decorator |
| Notify multiple objects of changes | Observer |
| Ensure single instance | Singleton |
| Convert incompatible interfaces | Adapter |
| Define algorithm skeleton | Template Method |
Creational Patterns
Builder
Use when: Object has many parameters, some optional.
public class User {
public User(String name) { }
public User(String name, String email) { }
public User(String name, String email, int age) { }
public User(String name, String email, int age, String phone) { }
}
public class User {
private final String name;
private final String email;
private final int age;
private final String phone;
private final String address;
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public Builder {
(name, email);
}
{
String name;
String email;
;
;
;
{
.name = name;
.email = email;
}
Builder {
.age = age;
;
}
Builder {
.phone = phone;
;
}
Builder {
.address = address;
;
}
User {
();
}
}
}
User.builder(, )
.age()
.phone()
.build();
With Lombok:
@Builder
@Getter
public class User {
private final String name;
private final String email;
@Builder.Default private int age = 0;
private String phone;
}
Factory Method
Use when: Need to create objects without specifying exact class.
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Push: " + message);
}
}
public class NotificationFactory {
public static Notification create(String type) {
return switch (type.toUpperCase()) {
-> ();
-> ();
-> ();
-> ( + type);
};
}
}
NotificationFactory.create();
notification.send();
With Spring (preferred):
public interface NotificationSender {
void send(String message);
String getType();
}
@Component
public class EmailSender implements NotificationSender {
@Override public void send(String message) { }
@Override public String getType() { return "EMAIL"; }
}
@Component
public class SmsSender implements NotificationSender {
@Override public void send(String message) { }
@Override public String getType() { return "SMS"; }
}
@Component
public class NotificationFactory {
private final Map<String, NotificationSender> senders;
public NotificationFactory(List<NotificationSender> senderList) {
.senders = senderList.stream()
.collect(Collectors.toMap(
NotificationSender::getType,
Function.identity()
));
}
NotificationSender {
Optional.ofNullable(senders.get(type))
.orElseThrow(() -> ( + type));
}
}
Singleton
Use when: Exactly one instance needed (use sparingly!).
public enum DatabaseConnection {
INSTANCE;
private Connection connection;
DatabaseConnection() {
}
public Connection getConnection() {
return connection;
}
}
Connection conn = DatabaseConnection.INSTANCE.getConnection();
With Spring (preferred):
@Component
public class DatabaseConnection {
}
Warning: Singletons can be problematic:
- Hard to test (global state)
- Hidden dependencies
- Consider dependency injection instead
Behavioral Patterns
Strategy
Use when: Multiple algorithms for same operation, need to swap at runtime.
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Paid " + amount + " with card " + cardNumber);
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Paid " + amount + " via PayPal: " + email);
}
}
public class CryptoPayment implements PaymentStrategy {
private String walletAddress;
{
.walletAddress = walletAddress;
}
{
System.out.println( + amount + + walletAddress);
}
}
{
PaymentStrategy paymentStrategy;
{
.paymentStrategy = strategy;
}
{
paymentStrategy.pay(total);
}
}
();
cart.setPaymentStrategy( ());
cart.checkout( ());
cart.setPaymentStrategy( ());
cart.checkout( ());
With Java 8+ (functional):
@FunctionalInterface
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
PaymentStrategy creditCard = amount ->
System.out.println("Card payment: " + amount);
PaymentStrategy paypal = amount ->
System.out.println("PayPal payment: " + amount);
cart.setPaymentStrategy(creditCard);
Observer
Use when: Objects need to be notified of changes in another object.
public interface OrderObserver {
void onOrderPlaced(Order order);
}
public class OrderService {
private final List<OrderObserver> observers = new ArrayList<>();
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void removeObserver(OrderObserver observer) {
observers.remove(observer);
}
public void placeOrder(Order order) {
saveOrder(order);
observers.forEach(observer -> observer.onOrderPlaced(order));
}
}
public class InventoryService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
order.getItems().forEach(item ->
reduceStock(item.getProductId(), item.getQuantity())
);
}
}
public class EmailNotificationService implements OrderObserver {
{
sendConfirmationEmail(order.getCustomerEmail(), order);
}
}
{
{
trackOrderEvent(order);
}
}
();
orderService.addObserver( ());
orderService.addObserver( ());
orderService.addObserver( ());
With Spring Events (preferred):
public record OrderPlacedEvent(Order order) {}
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void placeOrder(Order order) {
saveOrder(order);
eventPublisher.publishEvent(new OrderPlacedEvent(order));
}
}
@Component
public class InventoryListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
}
}
@Component
public class EmailListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
}
@EventListener
@Async
public void handleOrderPlacedAsync(OrderPlacedEvent event) {
}
}
Template Method
Use when: Define algorithm skeleton, let subclasses fill in steps.
public abstract class DataProcessor {
public final void process() {
readData();
processData();
writeData();
if (shouldNotify()) {
notifyCompletion();
}
}
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
protected boolean shouldNotify() {
return true;
}
protected void notifyCompletion() {
System.out.println("Processing completed!");
}
}
public class CsvDataProcessor extends DataProcessor {
@Override
protected void readData {
System.out.println();
}
{
System.out.println();
}
{
System.out.println();
}
}
{
{
System.out.println();
}
{
System.out.println();
}
{
System.out.println();
}
{
;
}
}
();
csvProcessor.process();
();
apiProcessor.process();
Structural Patterns
Decorator
Use when: Add behavior dynamically without modifying existing classes.
public interface Coffee {
String getDescription();
BigDecimal getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Coffee";
}
@Override
public BigDecimal getCost() {
return new BigDecimal("2.00");
}
}
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public BigDecimal getCost() {
return coffee.getCost();
}
}
{
{
(coffee);
}
String {
coffee.getDescription() + ;
}
BigDecimal {
coffee.getCost().add( ());
}
}
{
{
(coffee);
}
String {
coffee.getDescription() + ;
}
BigDecimal {
coffee.getCost().add( ());
}
}
{
{
(coffee);
}
String {
coffee.getDescription() + ;
}
BigDecimal {
coffee.getCost().add( ());
}
}
();
coffee = (coffee);
coffee = (coffee);
coffee = (coffee);
System.out.println(coffee.getDescription());
System.out.println(coffee.getCost());
Java I/O uses Decorator:
BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt")
)
);
Adapter
Use when: Make incompatible interfaces work together.
public interface MediaPlayer {
void play(String filename);
}
public class LegacyAudioPlayer {
public void playMp3(String filename) {
System.out.println("Playing MP3: " + filename);
}
}
public class AdvancedVideoPlayer {
public void playMp4(String filename) {
System.out.println("Playing MP4: " + filename);
}
public void playAvi(String filename) {
System.out.println("Playing AVI: " + filename);
}
}
public class Mp3PlayerAdapter implements MediaPlayer {
private final LegacyAudioPlayer legacyPlayer = new LegacyAudioPlayer();
@Override
public void play(String filename) {
legacyPlayer.playMp3(filename);
}
}
public {
();
{
(filename.endsWith()) {
videoPlayer.playMp4(filename);
} (filename.endsWith()) {
videoPlayer.playAvi(filename);
}
}
}
();
mp3Player.play();
();
videoPlayer.play();
Pattern Selection Guide
| Situation | Consider |
|---|
| Object creation is complex | Builder, Factory |
| Need to add features dynamically | Decorator |
| Multiple implementations of algorithm | Strategy |
| React to state changes | Observer |
| Integrate with legacy code | Adapter |
| Common algorithm, varying steps | Template Method |
| Need single instance | Singleton (use sparingly) |
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Better Approach |
|---|
| Singleton abuse | Global state, hard to test | Dependency Injection |
| Factory everywhere | Over-engineering | Simple new if type is known |
| Deep decorator chains | Hard to debug | Keep chains short, consider composition |
| Observer with many events | Spaghetti notifications | Event bus, clear event hierarchy |
Related Skills
solid-principles - Design principles that patterns help implement
clean-code - Code-level best practices
spring-boot-patterns - Spring-specific implementations