Why Most Backend Architecture Patterns Are Over‑engineered
A recent code review reveals a colleague using strategy, factory, and abstract‑factory patterns to build a payment system that only needs two methods, exposing how 90 % of classic backend architecture patterns become unnecessary over‑design in modern microservice and cloud‑native environments, and offering practical guidelines for when such complexity truly adds value.
Problem: Over‑engineered patterns in a simple order‑processing flow
A recent code review revealed a teammate using Strategy, Factory, and Abstract‑Factory patterns to handle only two payment methods (Alipay and WeChat). The design claims unlimited extensibility but adds unnecessary abstraction.
interface PaymentStrategy {
PayResult pay(Order order);
}
class PaymentStrategyFactory {
private Map<String, PaymentStrategy> strategies = new HashMap<>();
public PaymentStrategyFactory() {
strategies.put("alipay", new AlipayStrategy());
strategies.put("wechat", new WechatPayStrategy());
// ... theoretically unlimited extensions
}
public PaymentStrategy getStrategy(String type) {
return strategies.get(type);
}
}
interface PaymentFactory {
Validator createValidator();
Notifier createNotifier();
Logger createLogger();
}
public class OrderService {
private PaymentStrategyFactory strategyFactory;
private PaymentFactory paymentFactory;
public PayResult processOrder(Order order) {
PaymentStrategy strategy = strategyFactory.getStrategy(order.getPayType());
PaymentFactory factory = getFactory(order.getPayType());
factory.createValidator().validate(order);
PayResult result = strategy.pay(order);
factory.createNotifier().notify(result);
factory.createLogger().log(result);
return result;
}
}The author questioned why a simple if‑else or enum‑based strategy was not used, receiving the answer that the design is “more extensible” and “looks professional”.
Why classic patterns often misfit modern microservice backends
Two main drivers of over‑design:
Interview pressure – candidates memorize pattern names to answer “design a high‑concurrency system” questions.
Technical vanity – mentioning DDD, Event Sourcing, or CQRS is perceived as senior, even when unnecessary.
Abstract Factory
Classic use : create related objects without specifying concrete classes.
Issue in microservices : each service should own its data model and business logic; a cross‑service abstract factory creates a distributed monolith.
Modern practice : services keep their own factories or call other services via APIs/events.
@Service
public class OrderService {
@Autowired
private PaymentClient paymentClient;
public Order createOrder(CreateOrderRequest request) {
Order order = new Order();
order.setItems(request.getItems());
order.setTotal(request.getTotal());
// ... business logic
return orderRepository.save(order);
}
}Event Sourcing
Classic use : store every state‑changing event instead of the current state.
Issue : an e‑commerce order system rarely needs full event replay; a version column and audit logs are sufficient.
Modern practice : adopt only when strict audit trails or legal compliance demand it.
@Entity
public class Order {
@Id
private Long id;
private String status;
private BigDecimal amount;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
public void cancel() {
this.status = "CANCELLED";
this.updatedAt = LocalDateTime.now();
}
}CQRS (Command‑Query Responsibility Segregation)
Classic use : separate read and write models, often with different databases.
Issue : most business scenarios have a read/write ratio of 9:1 or 99:1; a read‑through cache plus a single database usually suffices.
Modern practice : start simple; introduce CQRS only after concrete performance bottlenecks appear.
@RestController
public class OrderController {
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable Long id) {
return cacheService.getOrLoad("order:" + id,
() -> orderRepository.findById(id).orElseThrow());
}
@PostMapping("/orders")
public Order createOrder(@RequestBody CreateOrderRequest request) {
Order order = orderService.createOrder(request);
cacheService.evict("order:" + order.getId());
return order;
}
}Over‑engineered DDD
Classic use : aggregates, value objects, domain services, repository interfaces, anti‑corruption layers.
Issue : a simple user‑management module does not need a full aggregate root, separate repository interface, and domain service.
Modern practice : keep DDD concepts (focus on domain logic) but avoid unnecessary layers.
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
private Long id;
private String username;
private String email;
@Embedded
private Address address;
public boolean canResetPassword() {
return !isLocked() && isActive();
}
public void changeEmail(String newEmail) {
validateEmail(newEmail);
this.email = newEmail;
this.emailVerified = false;
}
}Patterns that remain useful (≈10 % of the catalog)
Clear dependency direction
High‑level modules should depend on abstractions, not concrete low‑level modules. In Spring Boot, define service interfaces and inject implementations, but avoid abstraction for its own sake.
public interface PaymentService {
PaymentResult pay(PaymentRequest request);
RefundResult refund(RefundRequest request);
}
@Service
public class AlipayService implements PaymentService {
// concrete implementation
}
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}Simplified Strategy pattern
Eliminate long if‑else chains while keeping the code clear.
@Service
public class PaymentStrategy {
private final Map<String, PaymentProcessor> processors;
public PaymentStrategy(List<PaymentProcessor> processorList) {
processors = processorList.stream()
.collect(Collectors.toMap(PaymentProcessor::getType, Function.identity()));
}
public PaymentResult process(String type, PaymentRequest request) {
PaymentProcessor processor = processors.get(type);
if (processor == null) {
throw new IllegalArgumentException("Unsupported payment type: " + type);
}
return processor.process(request);
}
}Adapter pattern
Integrate third‑party SDKs while keeping the codebase tidy.
@Component
public class WechatPayAdapter implements PaymentProcessor {
private final WechatPayClient wechatPayClient;
@Override
public PaymentResult process(PaymentRequest request) {
WechatPayRequest wechatRequest = convertToWechatRequest(request);
WechatPayResponse response = wechatPayClient.pay(wechatRequest);
return convertToPaymentResult(response);
}
}Observer pattern for event‑driven flow
Decouple business logic using Spring’s event mechanism.
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public Order createOrder(CreateOrderRequest request) {
Order order = /* create order logic */;
eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
return order;
}
}
@Component
public class OrderEventListener {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
emailService.sendOrderConfirmation(event.getOrder());
inventoryService.updateStock(event.getOrder());
log.info("Order created: {}", event.getOrder().getId());
}
}Guidelines for code review
Prefer simplicity : use if‑else or direct calls when they suffice; avoid adding Strategy or Adapter layers unnecessarily.
Program for real change : design for actual business evolution, not imagined future features.
Practical layering : a three‑layer architecture (controller‑service‑repository) is often enough; avoid proliferating Manager/Processor/Handler layers.
Match architecture to scale : a system with 1 k daily active users does not need the infrastructure of a 10 M‑DAU service.
Code readability : ensure the next developer can understand and modify the code quickly.
When complex architecture is justified
Actual performance bottlenecks : e.g., database saturation, then consider sharding or CQRS.
Business complexity requiring DDD : e.g., inventory management, financial transaction processing.
Large team size : teams > 20 benefit from well‑defined module contracts.
High availability requirements : SLAs of 99.99 % or higher may warrant additional resilience layers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
