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.

Java Companion
Java Companion
Java Companion
Why Most Backend Architecture Patterns Are Over‑engineered

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendDesign Patternsarchitecturemicroservicescode reviewSpring Boot
Java Companion
Written by

Java Companion

A highly professional Java public account

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.