| name | design-patterns |
| description | GoF and enterprise design patterns for Spring Boot applications with Java examples |
Design Patterns for Spring Boot
Creational Patterns
Builder Pattern (with Records)
public record SearchCriteria(
String query,
List<String> categories,
BigDecimal minPrice,
BigDecimal maxPrice,
SortField sortBy,
SortDirection direction,
int page,
int size
) {
public static Builder builder() { return new Builder(); }
public static class Builder {
private String query;
private List<String> categories = List.of();
private BigDecimal minPrice;
private BigDecimal maxPrice;
private SortField sortBy = SortField.RELEVANCE;
private SortDirection direction = SortDirection.DESC;
private int page = 0;
private int size = 20;
public Builder query(String query) { this.query = query; return this; }
public Builder categories(List<String> categories) { this.categories = categories; return this; }
public Builder priceRange(BigDecimal min, BigDecimal max) {
this.minPrice = min; this.maxPrice = max; return this;
}
public Builder sortBy(SortField field, SortDirection dir) {
this.sortBy = field; this.direction = dir; return this;
}
public Builder page(int page, int size) {
this.page = page; this.size = size; return this;
}
public SearchCriteria build() {
return new SearchCriteria(query, categories, minPrice, maxPrice,
sortBy, direction, page, size);
}
}
}
Factory Pattern
public interface NotificationSender {
void send(Notification notification);
NotificationType supportedType();
}
@Component
public class EmailNotificationSender implements NotificationSender {
public void send(Notification notification) { }
public NotificationType supportedType() { return NotificationType.EMAIL; }
}
@Component
public class SmsNotificationSender implements NotificationSender {
public void send(Notification notification) { }
public NotificationType supportedType() { return NotificationType.SMS; }
}
@Component
public class NotificationSenderFactory {
private final Map<NotificationType, NotificationSender> senders;
public NotificationSenderFactory(List<NotificationSender> senderList) {
this.senders = senderList.stream()
.collect(Collectors.toMap(NotificationSender::supportedType, Function.identity()));
}
public NotificationSender getSender(NotificationType type) {
return Optional.ofNullable(senders.get(type))
.orElseThrow(() -> new UnsupportedOperationException(
"No sender for type: " + type));
}
}
Structural Patterns
Decorator Pattern
public interface OrderService {
OrderDto createOrder(CreateOrderRequest request);
}
@Service
@Primary
@RequiredArgsConstructor
public class LoggingOrderService implements OrderService {
private final OrderService delegate;
@Override
public OrderDto createOrder(CreateOrderRequest request) {
log.info("Creating order for customer={}", request.customerId());
var result = delegate.createOrder(request);
log.info("Order created with id={}", result.id());
return result;
}
}
@Service
@RequiredArgsConstructor
public class DefaultOrderService implements OrderService {
@Override
public OrderDto createOrder(CreateOrderRequest request) {
}
}
Adapter Pattern
public interface PaymentGateway {
PaymentResult charge(PaymentRequest request);
}
@Component
@RequiredArgsConstructor
public class StripePaymentAdapter implements PaymentGateway {
private final StripeClient stripeClient;
@Override
public PaymentResult charge(PaymentRequest request) {
var stripeRequest = StripeChargeRequest.builder()
.amount(request.amount().multiply(new BigDecimal("100")).longValue())
.currency(request.currency().toLowerCase())
.source(request.paymentMethodId())
.build();
var response = stripeClient.createCharge(stripeRequest);
return new PaymentResult(
response.getId(),
mapStatus(response.getStatus()),
request.amount());
}
private PaymentStatus mapStatus(String stripeStatus) {
return switch (stripeStatus) {
case "succeeded" -> PaymentStatus.COMPLETED;
case "pending" -> PaymentStatus.PENDING;
default -> PaymentStatus.FAILED;
};
}
}
Behavioral Patterns
Strategy Pattern
public interface PricingStrategy {
BigDecimal calculatePrice(Product product, Customer customer);
boolean appliesTo(CustomerType type);
}
@Component
public class RegularPricing implements PricingStrategy {
public BigDecimal calculatePrice(Product product, Customer customer) {
return product.getBasePrice();
}
public boolean appliesTo(CustomerType type) {
return type == CustomerType.REGULAR;
}
}
@Component
public class PremiumPricing implements PricingStrategy {
public BigDecimal calculatePrice(Product product, Customer customer) {
return product.getBasePrice().multiply(new BigDecimal("0.85"));
}
public boolean appliesTo(CustomerType type) {
return type == CustomerType.PREMIUM;
}
}
@Service
@RequiredArgsConstructor
public class PricingService {
private final List<PricingStrategy> strategies;
public BigDecimal getPrice(Product product, Customer customer) {
return strategies.stream()
.filter(s -> s.appliesTo(customer.getType()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No pricing strategy"))
.calculatePrice(product, customer);
}
}
Observer Pattern (Spring Events)
public record OrderStatusChangedEvent(
String orderId,
OrderStatus previousStatus,
OrderStatus newStatus,
Instant occurredAt
) {}
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
@Transactional
public void updateStatus(String orderId, OrderStatus newStatus) {
var order = orderRepository.findById(orderId).orElseThrow();
var previous = order.getStatus();
order.setStatus(newStatus);
orderRepository.save(order);
publisher.publishEvent(new OrderStatusChangedEvent(
orderId, previous, newStatus, Instant.now()));
}
}
@Component
public class OrderStatusListeners {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void onStatusChanged(OrderStatusChangedEvent event) {
}
}
Template Method Pattern
public abstract class AbstractDataImporter<T> {
public final ImportResult importData(InputStream source) {
var rawRecords = parseSource(source);
var validRecords = rawRecords.stream()
.filter(this::validate)
.map(this::transform)
.toList();
return persist(validRecords);
}
protected abstract List<Map<String, String>> parseSource(InputStream source);
protected abstract boolean validate(Map<String, String> record);
protected abstract T transform(Map<String, String> record);
protected abstract ImportResult persist(List<T> records);
}
@Component
public class ProductCsvImporter extends AbstractDataImporter<Product> {
@Override
protected List<Map<String, String>> parseSource(InputStream source) { }
@Override
protected boolean validate(Map<String, String> record) { }
@Override
protected Product transform(Map<String, String> record) { }
@Override
protected ImportResult persist(List<Product> records) { }
}
Pattern Selection Guide
| Problem | Pattern | Spring Integration |
|---|
| Multiple implementations | Strategy | List<Interface> injection |
| Object creation logic | Factory | @Component factory with DI |
| Cross-cutting concerns | Decorator | @Primary + delegation |
| External system integration | Adapter | @Component adapter |
| React to state changes | Observer | ApplicationEventPublisher |
| Complex object construction | Builder | Static builder() method |
| Step-based processing | Template Method | Abstract class with final template |