Fundamentals 16 min read

Why Knowing Hundreds of Design Patterns Still Breaks Your Code—and How an Engineering Decision Method Helps

Many developers learn dozens of design patterns but still end up with fragile, hard‑to‑maintain code; the article explains that the real key is to identify system problems first and apply a disciplined engineering decision method, illustrated with concrete Factory, Builder, Strategy, Template, Decorator, Adapter, Observer and Mediator examples.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Why Knowing Hundreds of Design Patterns Still Breaks Your Code—and How an Engineering Decision Method Helps

Problem‑driven pattern selection

Design patterns should be introduced only when a concrete structural or behavioral problem is identified. The article defines four recurring problem categories and the patterns that naturally address them:

Object creation out of control – symptoms: scattered new calls, duplicated creation logic, long constructor parameter lists, if‑else deciding concrete types, and the need to modify existing code for every new implementation. Common patterns: Factory, Builder.

Frequent behavior changes – symptoms: extensive if‑else chains, behavior logic spread across many places. Common patterns: Strategy, Template Method.

Rigid structure extension – symptoms: explosion of inheritance hierarchy, difficulty adding new variations. Common patterns: Decorator, Adapter.

Messy module communication – symptoms: strong coupling, tangled call graphs, hard‑to‑maintain dependencies. Common patterns: Observer, Mediator.

Before introducing any pattern, ask four engineering questions:

Does it reduce duplicate code?

Does it lower module coupling?

Does it improve extensibility?

Does it make the system easier to understand?

If the answer to any question is no , the pattern likely adds pseudo‑complexity and should be avoided.

1. Factory – isolating object creation

Problem: a payment service contains multiple if (type.equals(...)) branches that instantiate concrete processors ( UpiProcessor, CardProcessor, etc.). Adding a new channel forces changes to the same method, violating the Open/Closed Principle.

package com.icoderoad.payment;
public class PaymentService {
    public PaymentProcessor getProcessor(String type) {
        if ("UPI".equals(type)) { return new UpiProcessor(); }
        if ("CARD".equals(type)) { return new CardProcessor(); }
        if ("NETBANKING".equals(type)) { return new NetBankingProcessor(); }
        throw new IllegalArgumentException("unsupported payment type");
    }
}

Refactoring with a static factory centralises creation logic:

package com.icoderoad.payment.factory;
public class PaymentFactory {
    public static PaymentProcessor create(String type) {
        switch (type) {
            case "UPI": return new UpiProcessor();
            case "CARD": return new CardProcessor();
            case "NETBANKING": return new NetBankingProcessor();
            default: throw new IllegalArgumentException("unsupported type");
        }
    }
}

Business code now simply calls:

PaymentProcessor processor = PaymentFactory.create(type);
processor.process();

Benefits: creation logic is isolated, extension cost drops, duplicate code disappears, and the business layer no longer depends on concrete implementations.

2. Builder – controlling construction of complex objects

Problem: an Order class with many fields leads to a long constructor signature ( new Order(id, items, address, payment, true)), making parameter order hard to remember and causing bugs.

public class Order {
    private String id;
    private List<Item> items;
    private Address address;
    private Payment payment;
    private boolean priority;
}

Introducing a builder yields a fluent, readable construction:

Order order = new OrderBuilder()
    .withItems(items)
    .withAddress(address)
    .withPayment(payment)
    .priority(true)
    .build();

Benefits: clear parameter semantics, immutable result, avoidance of constructor pollution, and a controllable build flow.

3. Strategy – isolating frequently changing behavior

Problem: a pricing service uses a long if‑else chain to handle regular, premium, festive, VIP, regional, and time‑limited discounts. The chain quickly exceeds 300 lines as new rules are added, making the method unmaintainable.

public double calculatePrice(Order order) {
    if (order.getType() == OrderType.REGULAR) { return basePrice(order); }
    if (order.getType() == OrderType.PREMIUM) { return basePrice(order) * 0.9; }
    if (order.getType() == OrderType.FESTIVE) { return basePrice(order) * 0.8; }
    // … many more branches …
    return basePrice(order);
}

Extract a PricingStrategy interface and concrete strategy classes:

package com.icoderoad.pricing.strategy;
public interface PricingStrategy { double calculate(Order order); }

package com.icoderoad.pricing.strategy.impl;
public class PremiumStrategy implements PricingStrategy {
    @Override public double calculate(Order order) { return order.basePrice() * 0.9; }
}

At runtime a map selects the appropriate strategy:

PricingStrategy strategy = strategyMap.get(order.getType());
double price = strategy.calculate(order);

System benefits: adding a new discount rule requires only a new strategy class; existing code remains untouched, the giant if‑else disappears, and behavior is cleanly isolated.

4. Template Method – stable workflow with variable steps

Frameworks often need a fixed overall process while allowing subclasses to customise specific steps. Example from an order processing framework:

public abstract class OrderProcessor {
    public final void process() { validate(); calculate(); notifyUser(); }
    protected abstract void calculate();
}

Because the skeleton is final, the execution order is guaranteed, yet subclasses can provide different calculation logic. This pattern is widely used in Spring, Tomcat, and servlet implementations.

5. Decorator – composing behaviours without inheritance explosion

Problem: a notification system needs logging, retry, rate‑limiting, and monitoring. Naïve inheritance leads to exponential class growth ( EmailWithLoggingNotifier, EmailWithRetryNotifier, EmailWithLoggingAndRetryNotifier, …).

public class LoggingDecorator implements Notifier {
    private final Notifier notifier;
    public LoggingDecorator(Notifier notifier) { this.notifier = notifier; }
    @Override public void send() { log(); notifier.send(); }
    private void log() { System.out.println("record notify log"); }
}

Benefits: behaviours can be stacked at runtime, original notifier classes stay unchanged, and the inheritance hierarchy remains flat.

6. Adapter – reconciling incompatible interfaces

Problem: a third‑party API provides sendData(String payload) while the internal service expects send(Request request). Direct use creates a strong dependency on the external contract.

package com.icoderoad.adapter;
public class ApiAdapter implements InternalService {
    private final ThirdPartyApi api;
    public ApiAdapter(ThirdPartyApi api) { this.api = api; }
    @Override public void send(Request request) {
        String payload = convert(request);
        api.sendData(payload);
    }
    private String convert(Request request) { return ""; }
}

The adapter translates the incompatible structure into a collaborative one, allowing the internal code to remain stable.

7. Observer – decoupling modules via event publishing

Problem: an OrderService directly invokes payment, inventory, and notification services, creating a strong dependency chain.

public class OrderService {
    public void placeOrder() {
        paymentService.pay();
        inventoryService.update();
        notificationService.notifyUser();
    }
}

Refactor to publish an OrderCreatedEvent and let interested modules subscribe:

eventPublisher.publish(new OrderCreatedEvent());

Subscribers (payment, inventory, notification) handle the event independently. Benefits: reduced coupling, support for asynchronous extensions, and clearer module boundaries. This principle underlies Kafka, Spring Event, and other message‑driven architectures.

8. Mediator – centralising coordination in complex systems

When many services start calling each other, the call graph becomes a mesh. Introducing a mediator replaces the mesh with a single orchestrator:

public class OrderMediator {
    public void coordinate(Order order) {
        payment.process(order);
        inventory.reserve(order);
        logistics.dispatch(order);
    }
}

The mediator encapsulates coordination logic, preventing tangled dependencies.

Engineering benefit requirement

Every pattern must deliver measurable engineering value (e.g., reduced duplication, lower coupling, improved extensibility, or clearer understanding). Otherwise the pattern becomes “pseudo‑architecture”.

Typical immature evolution vs. mature evolution

Immature projects often start with an interface → factory → strategy → abstract class sequence, adding layers before the problem is clear, which makes the system heavier.

A mature system evolves in the opposite direction: first recognise the concrete constraint, then introduce the minimal structure (factory, builder, strategy, etc.) that solves it.

Thus, design patterns are not a checklist to memorize; they are natural outcomes of solving concrete engineering problems. The core skill of a senior engineer is to apply an engineering decision method that keeps the system evolvable, rather than applying patterns for their own sake.

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.

Design PatternsStrategy PatternSoftware EngineeringFactory Patternmediator patternobserver patternBuilder Patternengineering decision method
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

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.