We close the module with a question that is not about technique but about process: if the PideYa team works in two-week sprints, delivering in each one the minimum functionality that adds value, when does design happen? Do you draw the patterns up front, or let them appear? The tension between designing well and delivering soon is as old as agility itself, and patterns sit right at its center: they are the perfect design tool... and the perfect over-engineering temptation (we saw it in 05-05). This lesson teaches how to resolve that tension: how patterns emerge from the TDD cycle, how they make code testable, when to bet on one in the middle of a sprint, and how they become the team's language.
Contents
- Emergent design vs. Big Design Up Front
- TDD and patterns: the refactor step as cradle
- Patterns that make testing easier
- YAGNI vs. anticipation: when to bet
- Patterns as the team's language: reviews and ADRs
- Case study: one user story, three sprints
Emergent design vs. Big Design Up Front
Big Design Up Front (BDUF) designs the entire architecture before writing code: diagrams of every pattern, every interface, every case. Its problem is not designing — it is designing with the worst-quality information: the start of the project is when you know the least about the domain. PideYa's BDUF would have designed an Abstract Factory of payment gateways for eight countries... and then the business only opened in two.
Emergent design flips the bet: you write the minimum that solves the current story, with tests, and the design is continuously refined through refactoring. Patterns are not placed: they are discovered when the code asks for them. Beware the caricature: emergent design does not mean "no design" — it means continuous design, with judgment at every step. Kent Beck's four rules of simple design organize that judgment: (1) the tests pass, (2) it reveals intent, (3) no duplication, (4) as small as possible.
| BDUF | Emergent design | |
|---|---|---|
| When a pattern is decided | Before coding | When the code asks for it |
| Typical risk | Patterns for futures that never arrive (patternitis) | Debt if you don't actually refactor |
| Available information | Minimal | Maximal (real code and tests in front of you) |
| Requires | Predicting the future correctly | Refactoring and testing discipline |
TDD and patterns: the refactor step as cradle
The TDD cycle — red (failing test) → green (minimal code that passes) → refactor (improve without changing behavior) — has an exact moment where patterns are born: the refactor. In green being clever is forbidden; in refactor, with the test net strung up, you look at the code and apply the method from 05-01: which force is hurting? which pattern resolves it?
A real example from the PideYa team. Sprint N, story "charge by card": in green a CardCharge class is enough. Sprint N+1, story "charge with Bizum": the second test leads to an if (method == BIZUM). Sprint N+2, "charge with PayPal": third if. Now the code hurts (rule 3: duplication of the charging skeleton) and the refactor extracts the ChargeMethod interface — the Strategy from 04-10 has emerged, with three real implementations and not one speculative.
We saw the other half of the link in 05-04: when the code has no tests, characterization tests (pinning down the current behavior before touching anything) are the prerequisite for refactoring toward patterns. TDD gives you that net from the start; characterization rebuilds it after the fact. In both cases the sequence is the same: test net first, pattern second.
Patterns that make testing easier
The relationship runs both ways: TDD makes patterns emerge, and certain patterns make TDD possible. The three mechanisms, with their test doubles in JUnit:
1. DIP + injection → test doubles. If CheckoutFacade receives its ports through the constructor (06-01), the test replaces them with mocks:
@Test
void checkoutRejectedWhenChargeFails() {
PaymentGateway gateway = mock(PaymentGateway.class); // double for the port
Notifier notifier = mock(Notifier.class);
when(gateway.charge(any(), any()))
.thenReturn(ChargeResult.rejected("insufficient funds"));
var checkout = new CheckoutFacade(gateway, notifier); // injection: the seam
assertThrows(ChargeRejectedException.class, () -> checkout.confirmOrder(testOrder()));
verify(notifier, never()).send(any()); // don't notify what wasn't charged
}Without injection, CheckoutFacade would call new RedsysAdapter() internally and the test would need... a Redsys account. The injected interface is the point where the test gets in.
2. Strategy → fakes. A fake is a double with simplified real logic. The InMemoryGateway from 06-01 is a fake of the payments port: it charges "for real" against an in-memory map, and it works for fast integration tests without mocking every call. Every Strategy admits a fake strategy: a FixedDeliveryFeeCalculation that always returns €2.99 makes the checkout tests deterministic.
3. Facades → test seams. A seam (Michael Feathers) is a point where you can change behavior without editing the code. CheckoutFacade is the perfect seam for the web layer's tests: the controller tests mock the whole facade and don't drag in the checkout subsystem; the subsystem tests hit the real facade with fakes at the ports. Every well-designed boundary (facade, port, strategy) is a place where a test can grab hold of the system.
Diagnostic rule: code that is hard to test is badly designed code. If testing a class requires a database, the network, and a mutated Singleton (the Singletonitis of 05-05), the test is screaming at you which pattern is missing.
YAGNI vs. anticipation: when to bet
YAGNI ("You Aren't Gonna Need It"): don't build for hypothetical future needs. But does that mean you never anticipate anything? The practical answer distinguishes two things that are usually confused:
- Speculative flexibility (expensive to add, expensive to remove): hierarchies, factories, and layers "just in case". Here YAGNI wins almost always — removing an unnecessary Abstract Factory costs more than adding it when it's needed.
- Cheap seams (almost free to add, extremely valuable later): receiving dependencies through the constructor, programming against interfaces at the boundaries (DB, network, third parties), never leaving an infrastructure
newburied in the domain. That is not speculating: it is not nailing doors shut.
A guide for deciding in the middle of a sprint:
| Signal | Decision |
|---|---|
| The need belongs to the current story | Apply the pattern now, guilt-free |
| "Probably in some future sprint..." | YAGNI: simple code + a cheap seam |
| Second time the variation shows up | Prepare the refactor (see the rule of three) |
| Third time | Refactor to the pattern: you now have three real cases validating it |
| It's a boundary with the outside (payment, delivery, third parties) | Interface from day 1: the cost is one line |
The rule of three (Fowler) is the operational middle ground between YAGNI and anticipation: at the third repetition, the pattern pays for itself — and it is the agile version of the "wait until it hurts" from 05-01.
Patterns as the team's language
The most-cited advantage of patterns since 01-06 — the shared vocabulary — is, in agile, a process tool:
- In code reviews: "this is asking for a Strategy" conveys a complete redesign in five words; "this mediator is getting fat" (the god-mediator of 05-05) is an alarm everyone understands. A review comment with a pattern name is verifiable: the reviewee can go to the catalog and check whether the forces fit.
- In lightweight ADRs (Architecture Decision Records): one Markdown file per decision, in the repo itself, with four fields. PideYa's circuit-breaker one:
# ADR-014: Circuit breaker on calls to Payments
Status: accepted (sprint 23)
Context: two cascading-failure incidents when Payments degrades (>5 s response).
Decision: Resilience4j with threshold 5 failures / 10 s wait; fallback = enqueue deferred charge.
Consequences: checkout always responds in <3 s; a "payment pending" status appears
that Support must know about; discarded alternative: retries without a fuse (amplifies the load).Ten lines written in ten minutes. Their value shows up two years later, when someone asks "why is there a circuit breaker here?" and the answer doesn't depend on anyone's memory. In agile, where design is decided in small doses and often, ADRs are the memory of emergent design.
Case study: one user story, three sprints
A story from PideYa's backlog: "As a restaurant I want to offer 2-for-1 on pizzas on Tuesdays to attract weekday orders". Watch the design evolve:
Sprint 1 — the simple thing that works. TDD: test twoPizzasOnTuesdayChargeAsOne(). In green, the minimal solution is a discount hard-coded into the total calculation: an if (isTuesday() && ...). In refactor it is extracted into a named method (applyTuesdayPromotion), and it stops there. Zero patterns. Correct: one promotion does not justify an architecture. One seam is noted: the total calculation already receives an injected clock (Clock), because otherwise the Tuesday test would only pass on Tuesdays — the cheap seam YAGNI does allow.
Sprint 2 — second variation. New story: "20% off sushi on Sundays". Second if. The rule of three says: not yet, but prepare the ground — the tests of both promotions are organized so the imminent refactor won't break them (they test the result, not the internal structure).
Sprint 3 — the pattern emerges. Third story: "free delivery over €30 on the first order". Three promotions, three conditions, three effects: the code hurts in a recognizable way. The cycle's refactor: Promotion emerges as an interface with one implementation per promotion — Strategy — and the active promotions are applied in a chain over the order. In the code review someone points out that if the business wants restaurants to write their own rules, the next step would be the promotions-rules Interpreter we already built in 04-04. A five-line ADR is written: "Strategy now; Interpreter only if the 'restaurant-configurable rules' story enters the backlog". The big pattern is documented and deferred: that is designing with YAGNI, not against it.
The result seen across three sprints: the same destination a BDUF would have wanted to jump to on day one — but arriving with three real promotions validating the design, tests protecting it, and not one surplus abstraction along the way.
Common Mistakes and Tips
- Confusing emergent design with not designing: systematically skipping the refactor step "because we're in a hurry" piles up the
ifs forever. The refactor is not optional: it is where design happens. - BDUF disguised as sprint 0: spending three sprints "setting up the architecture" with every pattern before the first story. Deliver the first story on the bare minimum and let the architecture grow with the next ones.
- Mocking what isn't yours... and isn't a boundary: tests with seven chained mocks that break with every refactor. Mock at the seams (ports, facades), use real objects in the pure domain — which, being pure (immutable, no I/O, lesson 06-04), needs no doubles.
- Using YAGNI as an excuse to nail doors shut: refusing to inject a dependency "because YAGNI" confuses speculative flexibility with a cheap seam. The interface at the boundary costs one line; the buried
newcosts a sprint to dig out. - Pattern decisions with no trace: the pattern was applied, the why was forgotten, and two years later someone un-refactors it (05-05) because "it was surplus". Ten minutes of ADR prevent it.
- Imposing the pattern in review without the forces: "I'd put a Visitor here" without pointing at what hurts is opinion, not review. Name the force (duplication, boundary, variation) and then the pattern.
Exercises
- Classify the anticipations. For each decision in sprint 1 of a new PideYa feature ("tips for the courier"), say whether it is a cheap seam (do it) or speculative flexibility (YAGNI): (a) a
TipCalculatorinterface with a single fixed-percentage implementation; (b) receiving the tip repository through the constructor; (c) aTipAdded/TipModified/TipCancelledevent hierarchy with Event Sourcing "because orders already use it"; (d) an injectedClockfor the tip's timestamp. - Make the class testable.
InvoiceGeneratorinternally callsPideYaConfig.getInstance().getCurrentVat()andnew TaxOfficeClient().send(invoice). Explain why it is hard to test and refactor it (class signature and a sample test) using this lesson's patterns. - Write the ADR. In sprint 3 of the 2-for-1 case the team decides to apply Strategy and defer Interpreter. Draft the complete ADR (title, status, context, decision, consequences) in under 12 lines.
Solutions
- (a) A questionable boundary: if the calculation is a stable internal rule, an interface with a single implementation is speculative — YAGNI, extract it when the second variant arrives (rule of three); it is only defensible if you already know percentage/fixed/rounding lands next sprint. (b) Cheap seam: do it — it's one line and without it no unit test is possible. (c) Clear speculative flexibility: three event types and Event Sourcing for a feature that doesn't yet have a single real write; YAGNI. (d) Cheap seam: without an injected
Clockthe tests depend on the real time — do it. - It is hard to test because the Singleton couples it to global state (you can't vary the VAT per test without mutating it — Singletonitis) and the internal
newforces talking to the real tax office. Refactor:public InvoiceGenerator(TaxConfig config, InvoiceSender sender)— two injected ports (DIP);TaxConfigcan be implemented by the real configuration and by a test fake;InvoiceSenderis implemented byTaxOfficeClientand by a mock. Test:var gen = new InvoiceGenerator(configWithVat(21), mock(InvoiceSender.class)); var inv = gen.generate(orderOf(100)); assertEquals(new BigDecimal("121.00"), inv.total());— no network, no global state, deterministic. - A valid example:
# ADR-021: Strategy for promotions/Status: accepted (sprint 3)/Context: three promotions (2-for-1 Tuesdays, 20% sushi Sundays, free delivery >€30) implemented as ifs in the total calculation; duplication and regression risk when adding the fourth./Decision: a Promotion interface with one implementation per promotion, applied in a chain over the order; promotions registered through configuration./Consequences: adding a promotion = one class + one test, without touching the checkout; rules Interpreter (04-04) discarded for now: it will only be adopted if the "restaurant-configurable rules" story enters the backlog; the existing tests of the three promotions are kept as the safety net.
Conclusion
Patterns and agility do not compete: they need each other. Emergent design without a catalog is going in circles reinventing solutions with worse names; the catalog without emergent design is planned patternitis. The balance the PideYa team practices fits in four habits: letting patterns be born in the TDD cycle's refactor step, keeping cheap seams at the boundaries even when YAGNI trims everything else, applying the rule of three before abstracting, and leaving in writing — in a review, in a ten-line ADR — why each pattern is where it is.
This lesson closes module 6 and, with it, the technical journey of the course: the twenty-three GoF patterns built piece by piece on PideYa, the criteria for choosing them and not abusing them, and the modern catalog — architectures, microservices, distribution, concurrency, and agile process — where you recognized the same intents over and over, stretched across new problems. That is perhaps the deeper lesson: catalogs grow, but intents remain, and whoever masters them learns each new pattern in minutes. The learning journey, however, does not end here: in the final module we gather the best resources to continue it — starting with Recommended Books.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
