design-patterns

Design Patterns Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "design-patterns" with this command: npx skills add decebals/claude-code-java/decebals-claude-code-java-design-patterns

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.

// ❌ Telescoping constructor antipattern 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) { } // ... explosion of constructors }

// ✅ Builder pattern public class User { private final String name; // required private final String email; // required private final int age; // optional private final String phone; // optional private final String address; // optional

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 static Builder builder(String name, String email) {
    return new Builder(name, email);
}

public static class Builder {
    // Required
    private final String name;
    private final String email;
    // Optional with defaults
    private int age = 0;
    private String phone = "";
    private String address = "";

    private Builder(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public Builder age(int age) {
        this.age = age;
        return this;
    }

    public Builder phone(String phone) {
        this.phone = phone;
        return this;
    }

    public Builder address(String address) {
        this.address = address;
        return this;
    }

    public User build() {
        return new User(this);
    }
}

}

// Usage User user = User.builder("John", "john@example.com") .age(30) .phone("+1234567890") .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.

// ✅ Factory Method pattern 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); } }

// Factory public class NotificationFactory {

public static Notification create(String type) {
    return switch (type.toUpperCase()) {
        case "EMAIL" -> new EmailNotification();
        case "SMS" -> new SmsNotification();
        case "PUSH" -> new PushNotification();
        default -> throw new IllegalArgumentException("Unknown type: " + type);
    };
}

}

// Usage Notification notification = NotificationFactory.create("EMAIL"); notification.send("Hello!");

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&#x3C;NotificationSender> senderList) {
    this.senders = senderList.stream()
        .collect(Collectors.toMap(
            NotificationSender::getType,
            Function.identity()
        ));
}

public NotificationSender getSender(String type) {
    return Optional.ofNullable(senders.get(type))
        .orElseThrow(() -> new IllegalArgumentException("Unknown: " + type));
}

}

Singleton

Use when: Exactly one instance needed (use sparingly!).

// ✅ Modern singleton (enum-based, thread-safe) public enum DatabaseConnection { INSTANCE;

private Connection connection;

DatabaseConnection() {
    // Initialize connection
}

public Connection getConnection() {
    return connection;
}

}

// Usage Connection conn = DatabaseConnection.INSTANCE.getConnection();

With Spring (preferred):

@Component // Default scope is singleton public class DatabaseConnection { // Spring manages single instance }

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.

// ✅ Strategy pattern 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 final String walletAddress;

public CryptoPayment(String walletAddress) {
    this.walletAddress = walletAddress;
}

@Override
public void pay(BigDecimal amount) {
    System.out.println("Paid " + amount + " to wallet: " + walletAddress);
}

}

// Context public class ShoppingCart { private PaymentStrategy paymentStrategy;

public void setPaymentStrategy(PaymentStrategy strategy) {
    this.paymentStrategy = strategy;
}

public void checkout(BigDecimal total) {
    paymentStrategy.pay(total);
}

}

// Usage ShoppingCart cart = new ShoppingCart(); cart.setPaymentStrategy(new CreditCardPayment("4111-1111-1111-1111")); cart.checkout(new BigDecimal("99.99"));

// Change strategy at runtime cart.setPaymentStrategy(new PayPalPayment("user@example.com")); cart.checkout(new BigDecimal("49.99"));

With Java 8+ (functional):

// Strategy as functional interface @FunctionalInterface public interface PaymentStrategy { void pay(BigDecimal amount); }

// Usage with lambdas 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.

// ✅ Observer pattern (modern Java) 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) {
    // Process order
    saveOrder(order);

    // Notify all observers
    observers.forEach(observer -> observer.onOrderPlaced(order));
}

}

// Observers public class InventoryService implements OrderObserver { @Override public void onOrderPlaced(Order order) { // Reduce inventory order.getItems().forEach(item -> reduceStock(item.getProductId(), item.getQuantity()) ); } }

public class EmailNotificationService implements OrderObserver { @Override public void onOrderPlaced(Order order) { sendConfirmationEmail(order.getCustomerEmail(), order); } }

public class AnalyticsService implements OrderObserver { @Override public void onOrderPlaced(Order order) { trackOrderEvent(order); } }

// Setup OrderService orderService = new OrderService(); orderService.addObserver(new InventoryService()); orderService.addObserver(new EmailNotificationService()); orderService.addObserver(new AnalyticsService());

With Spring Events (preferred):

// Event public record OrderPlacedEvent(Order order) {}

// Publisher @Service public class OrderService { private final ApplicationEventPublisher eventPublisher;

public void placeOrder(Order order) {
    saveOrder(order);
    eventPublisher.publishEvent(new OrderPlacedEvent(order));
}

}

// Listeners (observers) @Component public class InventoryListener { @EventListener public void handleOrderPlaced(OrderPlacedEvent event) { // Reduce inventory } }

@Component public class EmailListener { @EventListener public void handleOrderPlaced(OrderPlacedEvent event) { // Send email }

@EventListener
@Async  // Async processing
public void handleOrderPlacedAsync(OrderPlacedEvent event) {
    // Send email asynchronously
}

}

Template Method

Use when: Define algorithm skeleton, let subclasses fill in steps.

// ✅ Template Method pattern public abstract class DataProcessor {

// Template method - defines the algorithm
public final void process() {
    readData();
    processData();
    writeData();
    if (shouldNotify()) {
        notifyCompletion();
    }
}

// Steps to be implemented by subclasses
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();

// Hook - optional override
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("Reading CSV file..."); }

@Override
protected void processData() {
    System.out.println("Processing CSV data...");
}

@Override
protected void writeData() {
    System.out.println("Writing to database...");
}

}

public class ApiDataProcessor extends DataProcessor { @Override protected void readData() { System.out.println("Fetching from API..."); }

@Override
protected void processData() {
    System.out.println("Transforming API response...");
}

@Override
protected void writeData() {
    System.out.println("Writing to cache...");
}

@Override
protected boolean shouldNotify() {
    return false;  // Override hook
}

}

// Usage DataProcessor csvProcessor = new CsvDataProcessor(); csvProcessor.process();

DataProcessor apiProcessor = new ApiDataProcessor(); apiProcessor.process();

Structural Patterns

Decorator

Use when: Add behavior dynamically without modifying existing classes.

// ✅ Decorator pattern 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");
}

}

// Base decorator 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();
}

}

// Concrete decorators public class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee coffee) { super(coffee); }

@Override
public String getDescription() {
    return coffee.getDescription() + ", Milk";
}

@Override
public BigDecimal getCost() {
    return coffee.getCost().add(new BigDecimal("0.50"));
}

}

public class SugarDecorator extends CoffeeDecorator { public SugarDecorator(Coffee coffee) { super(coffee); }

@Override
public String getDescription() {
    return coffee.getDescription() + ", Sugar";
}

@Override
public BigDecimal getCost() {
    return coffee.getCost().add(new BigDecimal("0.20"));
}

}

public class WhippedCreamDecorator extends CoffeeDecorator { public WhippedCreamDecorator(Coffee coffee) { super(coffee); }

@Override
public String getDescription() {
    return coffee.getDescription() + ", Whipped Cream";
}

@Override
public BigDecimal getCost() {
    return coffee.getCost().add(new BigDecimal("0.70"));
}

}

// Usage - compose decorators Coffee coffee = new SimpleCoffee(); coffee = new MilkDecorator(coffee); coffee = new SugarDecorator(coffee); coffee = new WhippedCreamDecorator(coffee);

System.out.println(coffee.getDescription()); // Coffee, Milk, Sugar, Whipped Cream System.out.println(coffee.getCost()); // 3.40

Java I/O uses Decorator:

// Classic example from Java BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream("file.txt") ) );

Adapter

Use when: Make incompatible interfaces work together.

// ✅ Adapter pattern

// Existing interface our code uses public interface MediaPlayer { void play(String filename); }

// Legacy/third-party interface 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);
}

}

// Adapters public class Mp3PlayerAdapter implements MediaPlayer { private final LegacyAudioPlayer legacyPlayer = new LegacyAudioPlayer();

@Override
public void play(String filename) {
    legacyPlayer.playMp3(filename);
}

}

public class VideoPlayerAdapter implements MediaPlayer { private final AdvancedVideoPlayer videoPlayer = new AdvancedVideoPlayer();

@Override
public void play(String filename) {
    if (filename.endsWith(".mp4")) {
        videoPlayer.playMp4(filename);
    } else if (filename.endsWith(".avi")) {
        videoPlayer.playAvi(filename);
    }
}

}

// Usage MediaPlayer mp3Player = new Mp3PlayerAdapter(); mp3Player.play("song.mp3");

MediaPlayer videoPlayer = new VideoPlayerAdapter(); videoPlayer.play("movie.mp4");

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

java-code-review

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

clean-code

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

spring-boot-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

architecture-review

No summary provided by upstream source.

Repository SourceNeeds Review