Why Over‑Engineered Architecture Fails: Real‑World Lessons from a Code Review

A senior architect recounts a code review where a colleague wrapped a simple two‑payment‑method order system in strategy, factory, and abstract‑factory patterns, then explains why such over‑design hurts maintainability, when to apply complex patterns, and practical guidelines for clean backend architecture.

Top Architect
Top Architect
Top Architect
Why Over‑Engineered Architecture Fails: Real‑World Lessons from a Code Review

Problem

A senior architect discovered an order‑processing module that only needed to support Alipay and WeChat payments, yet the code used Strategy, Factory, and Abstract‑Factory patterns and claimed to be easily extensible to twenty payment methods.

Why the design is over‑engineered

The excessive abstraction makes the code harder to understand and maintain while providing no tangible benefit for the current requirements.

Commonly misused patterns

Abstract Factory

Classic use : Provides an interface for creating related objects without specifying concrete classes.

Issue : In a microservice each service should own its own data and logic. Using a cross‑service abstract factory recreates a distributed monolith.

Modern practice : Keep factories inside a single service and use API calls or events for cross‑service interactions.

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());
    }
    public PaymentStrategy getStrategy(String type) {
        return strategies.get(type);
    }
}

Event Sourcing

Classic use : Stores every state‑changing event instead of the current state.

Issue : An e‑commerce order system rarely needs full replayability; a simple version column and audit log are sufficient.

Modern practice : Apply event sourcing only when strict audit or regulatory compliance demands 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 : Splits read and write models, often using separate databases.

Issue : For a typical 9:1 read‑write ratio a single database plus cache is enough.

Modern practice : Introduce CQRS only after profiling shows a genuine bottleneck.

@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;
    }
}

Heavy DDD

Classic use : Introduces aggregates, repositories, domain services, anti‑corruption layers, etc.

Issue : For a simple user‑management module such layers add noise without improving clarity.

Modern practice : Adopt DDD concepts (rich domain models, clear invariants) without forcing a heavyweight structure.

@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;
    }
}

Practical architectural guidelines

Keep dependency direction clear: high‑level modules depend on abstractions, not concrete low‑level modules.

Use a lightweight strategy map (e.g., Map<String, PaymentProcessor>) instead of a deep class hierarchy.

Apply the Adapter pattern only when integrating third‑party SDKs.

Leverage Spring’s event mechanism for side‑effects such as notifications, inventory updates, and logging.

Simplified strategy implementation

@Service
public class PaymentStrategy {
    private final Map<String, PaymentProcessor> processors;

    public PaymentStrategy(List<PaymentProcessor> processorList) {
        this.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 for a third‑party payment SDK

@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);
    }
}

Code review checklist (technical lead)

Prefer simplicity : Use if‑else or enums when they solve the problem; avoid unnecessary patterns.

Design for real change : Base architecture on observed requirement changes, not imagined futures.

Practical layering : A three‑layer architecture (controller, service, repository) is sufficient for most services.

Match technology to scale : Do not over‑provision infrastructure for low‑traffic systems.

Readability : Ensure the next developer can understand and modify the code quickly.

When complex architecture is justified

Actual performance bottlenecks (e.g., database saturation) require sharding, CQRS, or similar techniques.

The business domain is intrinsically complex (e.g., financial transaction processing, inventory management).

The team size exceeds a threshold where clear boundaries prevent coordination chaos.

High‑availability SLAs (e.g., 99.99% uptime) are mandatory.

Conclusion

Architecture complexity should be proportional to business complexity, not to a developer’s desire to showcase patterns. Modern cloud‑native, microservice, and serverless platforms already provide robust building blocks; the goal is to write simple, clear, maintainable code that adapts quickly to real business needs.

design-patternssoftware architecture
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.