| 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. Use when this capability is needed. |
| metadata | {"author":"decebals"} |
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
Converted and distributed by TomeVault — claim your Tome and manage your conversions.