If this course only taught you the 23 patterns, it would do you a disservice: you would walk away with a shiny new hammer and everything would look like a nail. The developer who has just learned patterns and applies them everywhere is such a real cliché that it has its own name in the profession ("patternitis"). This final lesson of the module introduces the counterweight: what teams that use patterns actually gain, what they pay for them, and — most valuable of all — the criteria for deciding, case by case, whether a pattern is worth it. With this scale well calibrated you will be ready to study the patterns one by one without losing your critical judgment.

Contents

  1. The real advantages of patterns
  2. The costs and risks
  3. The greatest risk: over-engineering
  4. Criteria for deciding whether to apply a pattern
  5. Summary table of the trade-off
  6. How to study the coming modules with this scale in mind

The real advantages of patterns

Shared vocabulary: the most underrated advantage

The most immediate gain is not in the code but in communication. Compare these two sentences in a PideYa team meeting:

"I made an interface with a calculation method, then several classes that implement it, one for each way of calculating the delivery fee, and the order class receives one of them and calls it without knowing which one it is..."

"The delivery-fee calculation is a Strategy."

The second sentence conveys in five words the complete structure, the responsibilities and even the expected consequences, because both parties share the catalog. This vocabulary operates everywhere: code reviews, documentation, class names (OrderBuilder, NotifierObserver), technical interviews and the documentation of the very libraries you use.

Proven solutions: not paying the rookie tax

A pattern condenses decades of trial and error by thousands of teams. When PideYa has to notify interested parties of order changes (the problem from lesson 1), we will not start from scratch: the cataloged solution has already solved the second-order problems you have not even seen coming yet (what if a subscriber fails? what if it unsubscribes during notification?). A pattern's "Implementation" and "Consequences" sections are ambushes already defused.

Maintainability and evolution

Patterns apply the design principles systematically, and that translates into code that absorbs changes: adding a payment gateway, a notification channel or a promotion without touching what works (OCP), and into testable code, because the abstractions patterns introduce are exactly the points where test doubles can be injected (DIP).

Readability for whoever comes next

A design with well-applied patterns is a self-documenting design for any trained developer: whoever opens the project and sees a CartMemento or a StripeGatewayAdapter knows what to expect before reading a single line of the body. It reduces the cost of onboarding new team members and the risk of "only John understands this part".

A bridge to frameworks and platforms

As we saw in the history lesson, frameworks are built with patterns. Knowing them turns the "magic" of Spring, JPA or Android into recognizable mechanisms, and lets you extend those frameworks through the extension points their authors designed (which are, almost always, exposed patterns).

The costs and risks

No pattern is free. The costs are real and worth facing head-on:

Accidental complexity and indirection

Every pattern adds pieces: interfaces, small classes, jumps between files. Where there was a method with three ifs, you may end up with an interface, four implementations and a factory. If the flexibility those pieces buy gets used, it is a good deal; if not, you have traded three readable ifs for seven files that must be opened in order to understand what happens. That complexity which comes not from the problem but from the solution is called accidental complexity, and it is the basic tax of patterns.

Learning curve and entry barrier

The shared vocabulary only works if the whole team speaks it. In a team where half the members do not know the patterns, a design full of them does not communicate: it intimidates. Pattern-heavy code is more readable for those who know them and less for those who do not; this is a real organizational cost you must weigh when deciding how much pattern your design introduces.

Applying a pattern where it does not belong

The most damaging risk is not implementing a pattern badly, but implementing the wrong pattern well (or an unnecessary one). Typical symptoms:

  • Choosing the pattern for the solution ("I feel like using a Builder") instead of for the problem.
  • Forcing the problem to fit the pattern, instead of the other way around.
  • Introducing a pattern's flexibility on an axis where the change never arrives (a YAGNI violation in elegant disguise).

When these mistakes become systematic, they crystallize into anti-patterns: recurring solutions that look good and are harmful. We will devote a full lesson to them in module 5 (Anti-Patterns); for now, just remember that they exist and that several are born from badly applied patterns.

Performance cost (minor, but it exists)

Indirection has a runtime cost (virtual calls, extra objects, memory). In 99% of the code of an application like PideYa it is negligible compared to the cost of a database query; only in extreme hot spots does it come to matter. It is the least important of the costs, but it is honest to mention it.

The greatest risk: over-engineering

It deserves its own section because it is the occupational disease of those who have just learned patterns. Over-engineering is building more structure than the problem needs. Let's see it in PideYa:

The real problem: PideYa needs to send a welcome email when a customer signs up. One email, simple, always the same.

The over-engineered solution (do not imitate this):

// Interface for "future flexibility"
public interface WelcomeStrategy { void execute(Customer c); }

// Abstract factory of strategies "in case there are more channels"
public interface WelcomeFactory { WelcomeStrategy create(); }

// Registry holding the single instance of the factory of factories...
public class FactoryRegistry { /* ... */ }

// ...and the only real implementation, hidden at the bottom:
public class EmailWelcome implements WelcomeStrategy {
    public void execute(Customer c) { /* send the email */ }
}

The proportionate solution:

public class SignupService {
    private final EmailService email;    // injected: testable (DIP)

    public SignupService(EmailService email) { this.email = email; }

    public void signUp(Customer customer) {
        // ...register the customer...
        email.sendWelcome(customer);
    }
}

The second version respects DIP (injected dependency, testable) without erecting any cathedral. If some day marketing asks for welcomes by SMS and email and push, configurable per country... that day there will be a real problem justifying more structure, and refactoring toward it will be easy precisely because the code is simple. Speculative flexibility is never free and almost never guesses the axis of future change correctly.

Golden rule: patterns are earned, not planted. You arrive at them when the problem demands it, not "just in case".

Criteria for deciding whether to apply a pattern

When tempted to apply a pattern, run it through these filters, in order:

  1. Can I name the problem without naming the pattern? Describe the problem and its forces in one sentence ("we need to add promotions without touching the calculation"). If all you can say is "I want to use X", you do not have a problem: you have an urge to use X.
  2. Is the change the pattern enables real or speculative? Has it already happened at least once, is it on the roadmap, or is it a hunch? Patterns pay off their cost when the axis of variation is real (PideYa's promotions change every month: real; "maybe some day we will support cryptocurrencies": hunch).
  3. Does the simple alternative already hurt? Look at the current code: are there concrete symptoms (duplication when adding cases, growing if chains, impossible tests, classes changing for unrelated reasons)? If the simple code does not hurt yet, it is usually too early.
  4. Will the team be able to maintain it? A design only you understand is a liability, not an asset. If you introduce a little-known pattern, escort it: name it in the code and comment the intent.
  5. Is the cost below the benefit? Count the pieces it adds (interfaces, classes, jumps) and compare them honestly with what it buys. In case of a tie, the simple option wins (KISS).

A compact decision flow:

flowchart TD
    A[Temptation to apply a pattern] --> B{Problem nameable<br/>without citing the pattern?}
    B -- No --> Z[Do not apply it:<br/>solution in search of a problem]
    B -- Yes --> C{Axis of change real<br/>or already hurting?}
    C -- No --> Y[Wait: simple code<br/>+ intent note]
    C -- Yes --> D{Benefit > cost<br/>and team ready?}
    D -- No --> Y
    D -- Yes --> E[Apply it: name it in the code<br/>and document the intent]

Notice the "Wait" box: not applying a pattern today is not giving up on it. It is keeping the code simple and clean so that, when the real change arrives, refactoring toward the pattern is cheap. In fact, the healthiest path to patterns is symptom-driven refactoring, and we will devote an entire lesson to it in module 5 (Refactoring with Design Patterns).

Summary table of the trade-off

Aspect Advantage Associated cost / risk
Communication Shared, precise team vocabulary Only works if everyone knows the catalog
Solution quality Decades of experience distilled; traps already solved False security if the wrong pattern is applied
Maintainability Localized changes (OCP), testable code (DIP) More classes and indirection to maintain
Readability Self-documenting design for those who know patterns Entry barrier for those who do not
Evolution Extension points prepared for real change Over-engineering if the change was speculative (YAGNI)
Relationship with frameworks You understand and extend your tools better
Performance Indirection with a marginal cost (rarely relevant)

And the synthesis in one sentence: a pattern is an investment: it buys flexibility and communication by paying complexity; it is only profitable if that flexibility gets used and that communication is shared.

How to study the coming modules with this scale in mind

From module 2 onward you will study concrete patterns, one per lesson. So that today's scale does not remain theory, adopt this discipline for every pattern:

  • Read the problem in PideYa first and try to solve it yourself, simply, before seeing the pattern's solution. That way you will feel what the pattern adds and what your naive solution adds.
  • When you reach the consequences, do not skip them: they are half the pattern. Always ask yourself "in which case would I NOT use it?". If you cannot answer, you do not know the pattern yet.
  • In the end-of-module comparisons, come back to this lesson's criteria: they are the referee between rival patterns.

Common Mistakes and Tips

  • The recent convert's "patternitis". After learning the patterns, everything seems to call for one. It is a normal phase; the vaccine is filter number 1: if you cannot name the problem without naming the pattern, there is no problem.
  • Measuring a design's quality by how many patterns it has. The metric is exactly the opposite: the best design is the simplest one that solves the problem and absorbs the real changes. Zero patterns can be the right answer.
  • Using the pattern as an excuse not to think. "A Singleton goes here because that's how it's always done" is not design, it is liturgy. Every application of a pattern must be defensible with the concrete problem and forces of the case.
  • Not naming the patterns in the code. If you apply a pattern, say so: OrderBuilder communicates; OrderManager2 hides. The vocabulary advantage is lost if the pattern stays camouflaged.
  • Rejecting patterns out of fear of over-engineering. The opposite pendulum swing is also a mistake: code with no abstraction at all, with duplication and chained ifs, is as expensive as the over-designed kind. The virtue lies not in "no patterns" but in "just the right patterns".
  • Tip: in your next design, write in a comment or in the pull request description which concrete future change justifies each abstraction you introduce. If you cannot write it, it is probably superfluous.

Exercises

Exercise 1: running a case through the filters

The PideYa team is debating whether to introduce a hierarchy of interchangeable strategies for the orders' VAT calculation. Facts: PideYa operates only in Spain, the VAT for food delivery has not changed in years, and there are no international expansion plans on the roadmap. Apply the five criteria from section 4 and issue a reasoned verdict.

Exercise 2: spotting the over-engineering

Point out which elements of this design for the "order out for delivery" alert are over-engineering, knowing that the only requirement is to send a push notification to the customer, and propose the proportionate version:

public interface NotificationChannel { void notify(Customer c, String msg); }
public interface ChannelFactory { NotificationChannel createChannel(String type); }
public class ChannelFactoryImpl implements ChannelFactory { /* one-case switch */ }
public class ChannelManager { /* keeps a registry of channel factories */ }
public class PushChannel implements NotificationChannel { /* the only existing channel */ }

Exercise 3: arguing the trade-off

Write two short paragraphs: one defending to your team the use of a pattern for PideYa's promotions (they change every month, more than one developer writes them), and another defending NOT using any pattern for the welcome email (a single one, stable for two years). In each paragraph cite at least one concrete advantage or cost from this lesson.

Solutions

Solution 1: (1) The problem is nameable: "calculate VAT according to rules that could vary"; it passes the first filter. (2) The axis of change is speculative: a single country, a rate stable for years, no expansion on the roadmap: it clearly fails. (3) The simple alternative (a constant or a calculateVat(total) method) does not hurt at all today. (4–5) Already irrelevant, but the cost (interface + implementations + injection) would exceed a nonexistent benefit. Verdict: do not apply the pattern; keep the calculation in a single well-named place (which DRY does demand) so that, if the platform ever goes international, the refactoring will be local and cheap.

Solution 2: ChannelFactory, ChannelFactoryImpl (a factory with a single-case switch) and ChannelManager (a registry of factories for one factory of one channel) are superfluous: three levels of indirection with no real variation to manage. Keeping the NotificationChannel interface with its single PushChannel implementation injected where used is defensible (minimal cost, eases testing), though even it is dispensable. Proportionate version:

public class AlertService {
    private final NotificationChannel channel;    // today, always PushChannel

    public AlertService(NotificationChannel channel) { this.channel = channel; }

    public void notifyOutForDelivery(Order order) {
        channel.notify(order.getCustomer(), "Your order is out for delivery!");
    }
}

If tomorrow SMS or email arrive as a real requirement, adding NotificationChannel implementations will be trivial; the factories will be introduced only if channel selection becomes a problem in its own right.

Solution 3 (possible write-ups):

In favor (promotions): "Promotions change every month and different people touch them: the axis of variation is real and frequent. Encapsulating each promotion behind a common abstraction gives us localized changes — adding a promotion will mean creating a class, without touching or re-testing the existing ones (OCP) — and a shared vocabulary: in reviews we will say 'it's a new promotion' and everyone will know what structure to expect. The cost in extra classes pays for itself within the first month."

Against (welcome email): "The welcome email is unique and has not changed in two years: there is no axis of variation to justify indirection. Building abstractions there would be speculative flexibility (YAGNI) and accidental complexity: more files to open to understand an email send. Let's keep it as a direct call, testable by injecting the email service; if the requirement ever grows, refactoring from simple code will be cheaper than maintaining an empty cathedral for years."

Conclusion

This lesson closes the introductory module, and with it you now have the full base kit: you know what a pattern is (context, problem, solution, consequences) and what it is not, where the discipline comes from (from Alexander to the GoF and beyond), which principles the patterns embody (SOLID, DRY, KISS, YAGNI and the GoF's two maxims), how to read their diagrams (UML with mermaid), how the catalog is organized (creational, structural and behavioral) and — today's lesson — how to weigh their advantages against their costs so you apply them with judgment and not out of fashion.

It is time to move from the foundations to the building's first floor. In module 2 we will tackle the first family of the catalog, the one that governs the birth of objects: how to create PideYa's pieces — payment gateways, notifiers, complex orders — without coupling the code to their concrete classes. See you in the Introduction to Creational Patterns.

© Copyright 2026. All rights reserved