| name | design-patterns |
| description | Use when implementing or reviewing code that benefits from established design patterns like Builder, Factory, Strategy, Observer, Decorator, or Adapter. |
Design Patterns for Java / Spring Boot
Builder Pattern
Use when constructing objects with many optional parameters.
public class SearchCriteria {
private final String keyword;
private final String category;
private final BigDecimal minPrice;
private final BigDecimal maxPrice;
private final Sort sort;
private final int page;
private final int size;
private SearchCriteria(Builder builder) {
this.keyword = builder.keyword;
this.category = builder.category;
this.minPrice = builder.minPrice;
this.maxPrice = builder.maxPrice;
this.sort = builder.sort;
this.page = builder.page;
this.size = builder.size;
}
public String getKeyword() { return keyword; }
public String getCategory() { return category; }
public BigDecimal getMinPrice() { return minPrice; }
public BigDecimal getMaxPrice() { return maxPrice; }
public Sort getSort() { return sort; }
public int getPage() { return page; }
public int getSize() { return size; }
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String keyword;
private String category;
private BigDecimal minPrice;
private BigDecimal maxPrice;
private Sort sort = Sort.unsorted();
private int page = 0;
private int size = 20;
public Builder keyword(String keyword) {
this.keyword = keyword;
return this;
}
public Builder category(String category) {
this.category = category;
return this;
}
public Builder priceRange(BigDecimal min, BigDecimal max) {
this.minPrice = min;
this.maxPrice = max;
return this;
}
public Builder sort(Sort sort) {
this.sort = sort;
return this;
}
public Builder page(int page) {
this.page = page;
return this;
}
public Builder size(int size) {
this.size = size;
return this;
}
public SearchCriteria build() {
return new SearchCriteria(this);
}
}
}
SearchCriteria criteria = SearchCriteria.builder()
.keyword("laptop")
.category("electronics")
.priceRange(BigDecimal.valueOf(500), BigDecimal.valueOf(2000))
.sort(Sort.by("price").ascending())
.page(0)
.size(10)
.build();
Factory Pattern
Use when object creation logic is complex or varies by type.
public interface Notification {
void send(String recipient, String message);
String getType();
}
@Component
public class EmailNotification implements Notification {
private final JavaMailSender mailSender;
public EmailNotification(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void send(String recipient, String message) {
SimpleMailMessage mail = new SimpleMailMessage();
mail.setTo(recipient);
mail.setSubject("Notification");
mail.setText(message);
mailSender.send(mail);
}
@Override
public String getType() { return "EMAIL"; }
}
@Component
public class SmsNotification implements Notification {
private final SmsClient smsClient;
public SmsNotification(SmsClient smsClient) {
this.smsClient = smsClient;
}
@Override
public void send(String recipient, String message) {
smsClient.sendSms(recipient, message);
}
@Override
public String getType() { return "SMS"; }
}
@Component
public class PushNotification implements Notification {
private final PushService pushService;
public PushNotification(PushService pushService) {
this.pushService = pushService;
}
@Override
public void send(String recipient, String message) {
pushService.sendPush(recipient, message);
}
@Override
public String getType() { return "PUSH"; }
}
@Component
public class NotificationFactory {
private final Map<String, Notification> notificationMap;
public NotificationFactory(List<Notification> notifications) {
this.notificationMap = notifications.stream()
.collect(Collectors.toMap(Notification::getType, Function.identity()));
}
public Notification getNotification(String type) {
Notification notification = notificationMap.get(type.toUpperCase());
if (notification == null) {
throw new IllegalArgumentException("Unknown notification type: " + type);
}
return notification;
}
}
@Service
public class AlertService {
private final NotificationFactory notificationFactory;
public AlertService(NotificationFactory notificationFactory) {
this.notificationFactory = notificationFactory;
}
public void alert(String type, String recipient, String message) {
Notification notification = notificationFactory.getNotification(type);
notification.send(recipient, message);
}
}
Strategy Pattern
Use when you need to swap algorithms or business rules at runtime.
public interface PricingStrategy {
BigDecimal calculatePrice(BigDecimal basePrice, int quantity);
String getName();
}
@Component
public class RegularPricing implements PricingStrategy {
@Override
public BigDecimal calculatePrice(BigDecimal basePrice, int quantity) {
return basePrice.multiply(BigDecimal.valueOf(quantity));
}
@Override
public String getName() { return "REGULAR"; }
}
@Component
public class BulkPricing implements PricingStrategy {
@Override
public BigDecimal calculatePrice(BigDecimal basePrice, int quantity) {
BigDecimal discount = quantity >= 100 ? BigDecimal.valueOf(0.20)
: quantity >= 50 ? BigDecimal.valueOf(0.10)
: quantity >= 20 ? BigDecimal.valueOf(0.05)
: BigDecimal.ZERO;
BigDecimal discountedPrice = basePrice.multiply(BigDecimal.ONE.subtract(discount));
return discountedPrice.multiply(BigDecimal.valueOf(quantity));
}
@Override
public String getName() { return "BULK"; }
}
@Component
public class SubscriptionPricing implements PricingStrategy {
@Override
public BigDecimal calculatePrice(BigDecimal basePrice, int quantity) {
BigDecimal subscriptionDiscount = BigDecimal.valueOf(0.15);
BigDecimal discountedPrice = basePrice.multiply(BigDecimal.ONE.subtract(subscriptionDiscount));
return discountedPrice.multiply(BigDecimal.valueOf(quantity));
}
@Override
public String getName() { return "SUBSCRIPTION"; }
}
@Component
public class PricingStrategyResolver {
private final Map<String, PricingStrategy> strategies;
public PricingStrategyResolver(List<PricingStrategy> strategyList) {
this.strategies = strategyList.stream()
.collect(Collectors.toMap(PricingStrategy::getName, Function.identity()));
}
public PricingStrategy resolve(String name) {
return strategies.getOrDefault(name.toUpperCase(), strategies.get("REGULAR"));
}
}
@Service
public class OrderPricingService {
private final PricingStrategyResolver resolver;
public OrderPricingService(PricingStrategyResolver resolver) {
this.resolver = resolver;
}
public BigDecimal calculateOrderTotal(String pricingType, List<OrderItem> items) {
PricingStrategy strategy = resolver.resolve(pricingType);
return items.stream()
.map(item -> strategy.calculatePrice(item.getUnitPrice(), item.getQuantity()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
Observer Pattern
Use Spring's event system instead of rolling your own.
public class OrderPlacedEvent {
private final Long orderId;
private final String customerEmail;
private final BigDecimal totalAmount;
private final LocalDateTime occurredAt;
public OrderPlacedEvent(Long orderId, String customerEmail, BigDecimal totalAmount) {
this.orderId = orderId;
this.customerEmail = customerEmail;
this.totalAmount = totalAmount;
this.occurredAt = LocalDateTime.now();
}
public Long getOrderId() { return orderId; }
public String getCustomerEmail() { return customerEmail; }
public BigDecimal getTotalAmount() { return totalAmount; }
public LocalDateTime getOccurredAt() { return occurredAt; }
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
public OrderService(OrderRepository orderRepository,
ApplicationEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
}
@Transactional
public OrderResponse placeOrder(OrderRequest request) {
Order order = createOrder(request);
Order saved = orderRepository.save(order);
eventPublisher.publishEvent(new OrderPlacedEvent(
saved.getId(),
request.customerEmail(),
saved.getTotalAmount()
));
return toResponse(saved);
}
}
@Component
public class OrderNotificationListener {
private final EmailService emailService;
public OrderNotificationListener(EmailService emailService) {
this.emailService = emailService;
}
@EventListener
public void onOrderPlaced(OrderPlacedEvent event) {
emailService.sendOrderConfirmation(event.getCustomerEmail(), event.getOrderId());
}
}
@Component
public class InventoryListener {
private final InventoryService inventoryService;
public InventoryListener(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@EventListener
@Async
public void onOrderPlaced(OrderPlacedEvent event) {
inventoryService.decrementStock(event.getOrderId());
}
}
@Component
public class AnalyticsListener {
private final AnalyticsService analyticsService;
public AnalyticsListener(AnalyticsService analyticsService) {
this.analyticsService = analyticsService;
}
@EventListener
@Async
public void onOrderPlaced(OrderPlacedEvent event) {
analyticsService.recordSale(event.getOrderId(), event.getTotalAmount());
}
}
Decorator Pattern
Use to dynamically add behavior to objects.
public interface DataExporter {
byte[] export(List<ReportRow> data);
String getContentType();
String getFileExtension();
}
@Component
public class CsvExporter implements DataExporter {
@Override
public byte[] export(List<ReportRow> data) {
StringBuilder sb = new StringBuilder();
sb.append("id,name,value,date\n");
for (ReportRow row : data) {
sb.append(String.format("%d,%s,%s,%s%n",
row.getId(), row.getName(),
row.getValue(), row.getDate()));
}
return sb.toString().getBytes(StandardCharsets.UTF_8);
}
@Override
public String getContentType() { return "text/csv"; }
@Override
public String getFileExtension() { return "csv"; }
}
public class CompressedExporter implements DataExporter {
private final DataExporter delegate;
public CompressedExporter(DataExporter delegate) {
this.delegate = delegate;
}
@Override
public byte[] export(List<ReportRow> data) {
byte[] raw = delegate.export(data);
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(baos)) {
gzip.write(raw);
gzip.finish();
return baos.toByteArray();
} catch (IOException e) {
throw new ExportException("Compression failed", e);
}
}
@Override
public String getContentType() { return "application/gzip"; }
@Override
public String getFileExtension() { return delegate.getFileExtension() + ".gz"; }
}
public class EncryptedExporter implements DataExporter {
private final DataExporter delegate;
private final EncryptionService encryptionService;
public EncryptedExporter(DataExporter delegate, EncryptionService encryptionService) {
this.delegate = delegate;
this.encryptionService = encryptionService;
}
@Override
public byte[] export(List<ReportRow> data) {
byte[] raw = delegate.export(data);
return encryptionService.encrypt(raw);
}
@Override
public String getContentType() { return "application/octet-stream"; }
@Override
public String getFileExtension() { return delegate.getFileExtension() + ".enc"; }
}
DataExporter exporter = new CompressedExporter(
new EncryptedExporter(new CsvExporter(), encryptionService));
byte[] output = exporter.export(reportData);
Adapter Pattern
Use to bridge incompatible interfaces, especially for third-party integrations.
public interface PaymentGateway {
PaymentResult charge(String customerId, BigDecimal amount, String currency);
PaymentResult refund(String transactionId, BigDecimal amount);
PaymentStatus checkStatus(String transactionId);
}
public record PaymentResult(String transactionId, PaymentStatus status, String message) {}
public enum PaymentStatus { SUCCESS, FAILED, PENDING, REFUNDED }
@Component
@Profile("stripe")
public class StripePaymentAdapter implements PaymentGateway {
private final StripeApi stripeApi;
public StripePaymentAdapter(StripeApi stripeApi) {
this.stripeApi = stripeApi;
}
@Override
public PaymentResult charge(String customerId, BigDecimal amount, String currency) {
StripeChargeRequest request = new StripeChargeRequest();
request.setCustomer(customerId);
request.setAmount(amount.multiply(BigDecimal.valueOf(100)).longValue());
request.setCurrency(currency.toLowerCase());
StripeChargeResponse response = stripeApi.createCharge(request);
return new PaymentResult(
response.getId(),
mapStatus(response.getStatus()),
response.getOutcomeMessage()
);
}
@Override
public PaymentResult refund(String transactionId, BigDecimal amount) {
StripeRefundResponse response = stripeApi.createRefund(transactionId,
amount.multiply(BigDecimal.valueOf(100)).longValue());
return new PaymentResult(
response.getId(),
PaymentStatus.REFUNDED,
"Refund processed"
);
}
@Override
public PaymentStatus checkStatus(String transactionId) {
StripeChargeResponse response = stripeApi.retrieveCharge(transactionId);
return mapStatus(response.getStatus());
}
private PaymentStatus mapStatus(String stripeStatus) {
return switch (stripeStatus) {
case "succeeded" -> PaymentStatus.SUCCESS;
case "failed" -> PaymentStatus.FAILED;
case "pending" -> PaymentStatus.PENDING;
default -> PaymentStatus.PENDING;
};
}
}
@Component
@Profile("paypal")
public class PayPalPaymentAdapter implements PaymentGateway {
private final PayPalClient payPalClient;
public PayPalPaymentAdapter(PayPalClient payPalClient) {
this.payPalClient = payPalClient;
}
@Override
public PaymentResult charge(String customerId, BigDecimal amount, String currency) {
PayPalOrder order = payPalClient.createOrder(amount.toString(), currency);
PayPalCapture capture = payPalClient.captureOrder(order.getOrderId());
return new PaymentResult(
capture.getCaptureId(),
"COMPLETED".equals(capture.getStatus()) ? PaymentStatus.SUCCESS : PaymentStatus.FAILED,
capture.getStatusMessage()
);
}
@Override
public PaymentResult refund(String transactionId, BigDecimal amount) {
PayPalRefund refund = payPalClient.refundCapture(transactionId, amount.toString());
return new PaymentResult(refund.getRefundId(), PaymentStatus.REFUNDED, "Refund processed");
}
@Override
public PaymentStatus checkStatus(String transactionId) {
PayPalCapture capture = payPalClient.getCapture(transactionId);
return "COMPLETED".equals(capture.getStatus()) ? PaymentStatus.SUCCESS : PaymentStatus.PENDING;
}
}