| 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. |
Design Patterns for Java
Quick Reference - When to Use What
| Problem | Pattern | Example |
|---|
| Complex object construction | Builder | Creating objects with many optional fields |
| Object creation logic varies | Factory Method | Different payment processors |
| Only one instance needed | Singleton | Configuration, connection pool |
| Swap algorithms at runtime | Strategy | Payment methods, sorting |
| React to state changes | Observer | Order events, notifications |
| Define algorithm skeleton | Template Method | Data processing pipelines |
| Add behavior dynamically | Decorator | Adding features to streams, logging |
| Incompatible interfaces | Adapter | Third-party API integration |
Creational Patterns
Builder Pattern
Problem: Telescoping Constructor Anti-Pattern
public class User {
public User(String firstName, String lastName) { ... }
public User(String firstName, String lastName, String email) { ... }
public User(String firstName, String lastName, String email, String phone) { ... }
public User(String firstName, String lastName, String email, String phone,
String address, int age, boolean active) { ... }
}
User user = new User("John", "Doe", "john@example.com", null, null, 30, true);
Solution: Builder
public class User {
private final String firstName;
private final String lastName;
private final String email;
private final String phone;
private final String address;
private final int age;
private final boolean active;
private User(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.email = builder.email;
this.phone = builder.phone;
this.address = builder.address;
this.age = builder.age;
this.active = builder.active;
}
public static Builder builder(String firstName, String lastName) {
return new Builder(firstName, lastName);
}
public static class Builder {
private final String firstName;
private final String lastName;
private String email;
private String phone;
private String address;
private int age;
private boolean active = true;
private Builder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder active(boolean active) {
this.active = active;
return this;
}
public User build() {
if (firstName == null || firstName.isBlank()) {
throw new IllegalStateException("firstName is required");
}
return new User(this);
}
}
}
User user = User.builder("John", "Doe")
.email("john@example.com")
.age(30)
.active(true)
.build();
Factory Method Pattern
public interface NotificationSender {
void send(String recipient, String message);
}
public class EmailSender implements NotificationSender {
@Override
public void send(String recipient, String message) {
}
}
public class SmsSender implements NotificationSender {
@Override
public void send(String recipient, String message) {
}
}
public class PushNotificationSender implements NotificationSender {
@Override
public void send(String recipient, String message) {
}
}
public class NotificationSenderFactory {
public static NotificationSender create(NotificationType type) {
return switch (type) {
case EMAIL -> new EmailSender();
case SMS -> new SmsSender();
case PUSH -> new PushNotificationSender();
};
}
}
Spring Variant - Using Dependency Injection
@Component
public class NotificationSenderFactory {
private final Map<NotificationType, NotificationSender> senders;
public NotificationSenderFactory(List<NotificationSender> senderList) {
this.senders = senderList.stream()
.collect(Collectors.toMap(
NotificationSender::getType,
Function.identity()
));
}
public NotificationSender getSender(NotificationType type) {
NotificationSender sender = senders.get(type);
if (sender == null) {
throw new UnsupportedNotificationTypeException(type);
}
return sender;
}
}
Singleton Pattern
public enum AppConfig {
INSTANCE;
private final Properties properties = new Properties();
AppConfig() {
try (InputStream is = getClass().getResourceAsStream("/config.properties")) {
properties.load(is);
} catch (IOException e) {
throw new RuntimeException("Failed to load config", e);
}
}
public String getProperty(String key) {
return properties.getProperty(key);
}
}
String dbUrl = AppConfig.INSTANCE.getProperty("db.url");
Spring Singleton (preferred in Spring applications)
@Component
public class AppConfig {
@Value("${db.url}")
private String dbUrl;
public String getDbUrl() {
return dbUrl;
}
}
@Service
public class UserService {
private final AppConfig config;
public UserService(AppConfig config) {
this.config = config;
}
}
Behavioral Patterns
Strategy Pattern
public interface PaymentStrategy {
PaymentResult pay(BigDecimal amount);
PaymentType getType();
}
@Component
public class CreditCardPayment implements PaymentStrategy {
@Override
public PaymentResult pay(BigDecimal amount) {
return new PaymentResult(true, "CC-" + UUID.randomUUID());
}
@Override
public PaymentType getType() {
return PaymentType.CREDIT_CARD;
}
}
@Component
public class PayPalPayment implements PaymentStrategy {
@Override
public PaymentResult pay(BigDecimal amount) {
return new PaymentResult(true, "PP-" + UUID.randomUUID());
}
@Override
public PaymentType getType() {
return PaymentType.PAYPAL;
}
}
@Service
public class PaymentService {
private final Map<PaymentType, PaymentStrategy> strategies;
public PaymentService(List<PaymentStrategy> strategyList) {
this.strategies = strategyList.stream()
.collect(Collectors.toMap(PaymentStrategy::getType, Function.identity()));
}
public PaymentResult processPayment(PaymentType type, BigDecimal amount) {
PaymentStrategy strategy = strategies.get(type);
if (strategy == null) {
throw new UnsupportedPaymentTypeException(type);
}
return strategy.pay(amount);
}
}
Functional Variant (for simple strategies)
@Service
public class DiscountService {
private static final Map<CustomerTier, UnaryOperator<BigDecimal>> DISCOUNT_STRATEGIES = Map.of(
CustomerTier.STANDARD, price -> price,
CustomerTier.SILVER, price -> price.multiply(new BigDecimal("0.95")),
CustomerTier.GOLD, price -> price.multiply(new BigDecimal("0.90")),
CustomerTier.PLATINUM, price -> price.multiply(new BigDecimal("0.85"))
);
public BigDecimal applyDiscount(BigDecimal price, CustomerTier tier) {
return DISCOUNT_STRATEGIES
.getOrDefault(tier, UnaryOperator.identity())
.apply(price);
}
}
Observer Pattern
public interface OrderObserver {
void onOrderPlaced(Order order);
}
@Component
public class InventoryObserver implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
}
}
@Component
public class EmailObserver implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
}
}
@Service
public class OrderService {
private final List<OrderObserver> observers;
public OrderService(List<OrderObserver> observers) {
this.observers = observers;
}
public Order placeOrder(CreateOrderRequest request) {
Order order = createOrder(request);
orderRepository.save(order);
observers.forEach(observer -> observer.onOrderPlaced(order));
return order;
}
}
Spring Events (preferred in Spring applications)
public record OrderPlacedEvent(Order order, Instant timestamp) {
public OrderPlacedEvent(Order order) {
this(order, Instant.now());
}
}
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
private final OrderRepository orderRepository;
public OrderService(ApplicationEventPublisher eventPublisher,
OrderRepository orderRepository) {
this.eventPublisher = eventPublisher;
this.orderRepository = orderRepository;
}
@Transactional
public Order placeOrder(CreateOrderRequest request) {
Order order = createOrder(request);
orderRepository.save(order);
eventPublisher.publishEvent(new OrderPlacedEvent(order));
return order;
}
}
@Component
public class OrderEventListeners {
@EventListener
public void handleInventoryUpdate(OrderPlacedEvent event) {
}
@Async
@EventListener
public void handleEmailNotification(OrderPlacedEvent event) {
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAnalytics(OrderPlacedEvent event) {
}
}
Template Method Pattern
public abstract class DataProcessor<T> {
public final void process() {
List<T> data = fetchData();
List<T> validated = validate(data);
List<T> transformed = transform(validated);
load(transformed);
notifyCompletion(transformed.size());
}
protected abstract List<T> fetchData();
protected abstract List<T> transform(List<T> data);
protected abstract void load(List<T> data);
protected List<T> validate(List<T> data) {
return data.stream()
.filter(this::isValid)
.toList();
}
protected boolean isValid(T item) {
return item != null;
}
protected void notifyCompletion(int count) {
log.info("Processed {} records", count);
}
}
@Component
public class CsvUserImporter extends DataProcessor<UserRecord> {
@Override
protected List<UserRecord> fetchData() {
return csvReader.readAll("users.csv");
}
@Override
protected List<UserRecord> transform(List<UserRecord> data) {
return data.stream()
.map(record -> record.withEmail(record.email().toLowerCase()))
.toList();
}
@Override
protected void load(List<UserRecord> data) {
userRepository.saveAll(data.stream()
.map(UserRecord::toEntity)
.toList());
}
@Override
protected boolean isValid(UserRecord item) {
return item != null && item.email() != null && item.email().contains("@");
}
}
Structural Patterns
Decorator Pattern
public interface Coffee {
String getDescription();
BigDecimal getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple coffee";
}
@Override
public BigDecimal getCost() {
return new BigDecimal("1.00");
}
}
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public BigDecimal getCost() {
return decoratedCoffee.getCost();
}
}
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", milk";
}
@Override
public BigDecimal getCost() {
return decoratedCoffee.getCost().add(new BigDecimal("0.50"));
}
}
public class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", whip";
}
@Override
public BigDecimal getCost() {
return decoratedCoffee.getCost().add(new BigDecimal("0.75"));
}
}
Coffee coffee = new WhipDecorator(new MilkDecorator(new SimpleCoffee()));
Adapter Pattern
public interface MediaPlayer {
void play(String filename);
}
public class VlcLibrary {
public void playVlc(String filename) {
}
}
public class FfmpegLibrary {
public void playFfmpeg(String filename) {
}
}
public class VlcAdapter implements MediaPlayer {
private final VlcLibrary vlcLibrary;
public VlcAdapter(VlcLibrary vlcLibrary) {
this.vlcLibrary = vlcLibrary;
}
@Override
public void play(String filename) {
vlcLibrary.playVlc(filename);
}
}
public class FfmpegAdapter implements MediaPlayer {
private final FfmpegLibrary ffmpegLibrary;
public FfmpegAdapter(FfmpegLibrary ffmpegLibrary) {
this.ffmpegLibrary = ffmpegLibrary;
}
@Override
public void play(String filename) {
ffmpegLibrary.playFfmpeg(filename);
}
}
public interface PaymentGateway {
PaymentResult charge(BigDecimal amount, String currency, String token);
}
@Component
public class StripeAdapter implements PaymentGateway {
private final StripeClient stripeClient;
@Override
public PaymentResult charge(BigDecimal amount, String currency, String token) {
ChargeCreateParams params = ChargeCreateParams.builder()
.setAmount(amount.multiply(BigDecimal.valueOf(100)).longValue())
.setCurrency(currency)
.setSource(token)
.build();
Charge charge = stripeClient.charges().create(params);
return new PaymentResult(charge.getId(), charge.getStatus());
}
}
Pattern Selection Guide
| Situation | Consider |
|---|
| Many constructor parameters | Builder |
| Object creation varies by type | Factory Method |
| Need to swap behavior at runtime | Strategy |
| React to events/state changes | Observer (Spring Events) |
| Algorithm with varying steps | Template Method |
| Add optional features/behaviors | Decorator |
| Integrate third-party library | Adapter |
| Ensure single instance | Singleton (or just use Spring @Component) |
| Complex object graphs | Builder + Factory |
| Multiple notification types | Strategy + Observer |
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Better Approach |
|---|
| God Object | One class does everything | SRP - split into focused classes |
| Spaghetti Code | Tangled control flow | Extract methods, use patterns |
| Golden Hammer | Using one pattern for everything | Choose pattern based on problem |
| Premature Optimization | Complex patterns for simple problems | Start simple, refactor when needed |
| Copy-Paste Programming | Duplicated code everywhere | Extract, use Template Method or Strategy |
| Singleton Abuse | Everything is a singleton | Use DI, limit true singletons |
| Interface Bloat | Huge interfaces | Interface Segregation Principle |
Related Skills
clean-code - Principles that guide pattern selection
java-code-review - Review pattern implementations
spring-boot-patterns - Spring-specific patterns and conventions