In modules 2 to 4 every pattern got its own lesson and its own corner of PideYa; in the previous lesson you learned the method for choosing them. This lesson brings both together and takes them to real scale: complete features where several patterns collaborate, and where the interesting part is no longer each piece but the seams — the exact point where one pattern ends and another begins, and why the boundary sits there and not somewhere else. We'll look at two cases: the complete flow of an order (six patterns already built, finally seen in a single pass) and a brand-new loyalty module built from scratch by applying the method — including the decisions not to use a pattern, which in real code are half the design.

Contents

  1. Case A: an order's complete journey — six patterns, five seams
  2. The seams, one by one
  3. Case B: the loyalty module, designed from scratch with the method
  4. The trade-offs: where we decided not to use a pattern
  5. Exercises and conclusion

Case A: an order's complete journey

Every pattern in this sequence has its own lesson; what we had never drawn is the whole. A customer taps "confirm order" and this is what happens:

sequenceDiagram
    participant App as Customer app
    participant F as CheckoutFacade
    participant CH as Validation chain
    participant S as DeliveryFeeCalculation (Strategy)
    participant B as Order.Builder
    participant P as Order (State)
    participant O as Observers

    App->>F: confirmOrder(cart, customer)
    F->>CH: handle(request)
    Note over CH: Fraud → Zone → Stock → Minimum<br/>(any of them can cut it short)
    CH-->>F: approved
    F->>S: calculate(cart, address)
    S-->>F: deliveryFee
    F->>B: build with lines, delivery fee, taxes
    B-->>F: Order (in CREATED state)
    F->>P: pay()
    Note over P: CreatedState authorizes<br/>and transitions to PAID
    P->>O: transitionTo broadcasts the change
    Note over O: CustomerNotifier, KitchenMonitor,<br/>StatsPanel... react
    F-->>App: OrderConfirmation

In text, the chain of responsibilities (lowercase) is this:

  1. Facade — CheckoutFacade is the single entry point: the app knows nothing about the chain, the strategies, or the builder. Its role: orchestrate and hide.
  2. Chain of Responsibility — the facade fires the validation (FraudHandler → DeliveryZoneHandler → StockHandler → MinimumAmountHandler). Its role: decide whether the order gets in, without the facade knowing how many filters there are.
  3. Strategy — DeliveryFeeCalculation computes the fee with the market's variant. Its role: an interchangeable calculation with no if in the facade.
  4. Builder — Order.Builder assembles the object with its lines, delivery fee and taxes, validating invariants in build(). Its role: complex construction outside the constructor.
  5. State — the order is born in CREATED and pay() transitions to PAID only if the state authorizes it. Its role: making each operation legal only when its time has come.
  6. Observer — transitionTo broadcasts the transition to customer, kitchen and stats. Its role: letting the interested parties find out without the order knowing them. Module 4's maxim still holds: State authorizes, Observer broadcasts.

The seams, one by one

What makes this a design and not a pile of patterns is where each boundary sits:

The facade–chain seam. The facade knows one object: the first link (injected already assembled, as configured in 04-02). If compliance demands a fifth filter tomorrow, it gets added to the chain's assembly and the facade doesn't change. The seam is the OrderHandler interface: above it, orchestration; below it, admission policy.

public class CheckoutFacade {

    private final OrderHandler validationChain;         // first link, already wired
    private final DeliveryFeeCalculation deliveryFee;   // the market's strategy
    private final EventLog events;

    public OrderConfirmation confirmOrder(Cart cart, Customer customer) {
        // Seam 1: the facade delegates admission and only looks at the result
        ValidationResult result = validationChain.handle(new OrderRequest(cart, customer));
        if (!result.approved()) {
            throw new OrderRejectedException(result.reason());
        }

        // Seam 2: the variable calculation lives outside; the facade only invokes it
        BigDecimal fee = deliveryFee.calculate(cart, customer.address());

        // Seam 3: the complex construction lives in the Builder, not here
        Order order = new Order.Builder(customer)
                .lines(cart.lines())
                .deliveryFee(fee)
                .build();                 // born in CREATED state

        // Seam 4: the facade requests the operation; the STATE decides if it is legal
        order.pay();                      // CREATED → PAID... and there seam 5 fires:
                                          // transitionTo broadcasts to the observers.
        return new OrderConfirmation(order);
    }
}

Notice what the facade does not do: it doesn't validate (chain), doesn't calculate (strategy), doesn't construct (builder), doesn't flip states by hand (State) and doesn't notify anyone (Observer). Ten lines of pure orchestration — every responsibility pushed out toward the pattern whose symptom claimed it. That is the proof that the seams are in the right place: the facade stays boring.

The State–Observer seam — the finest one in the system and the easiest to break. A single point connects them:

// Inside Order: the ONLY place where both patterns touch
void transitionTo(OrderState newState) {
    OrderState previous = this.state;
    this.state = newState;                            // State: the transition, already authorized
    observers.forEach(o -> o.orderUpdated(this, previous, newState)); // Observer
}

If an observer started causing transitions (the kitchen, on hearing about PAID, calling startPreparing() on the same order in the same thread), the broadcast would turn recursive and the notification order would become covert business logic — the sign that this reaction belongs in a Mediator or an explicit command, not in the observer.

Case B: the loyalty module, from scratch

The business asks: "a points program: every delivered order accrues points according to per-tier rules (bronze/silver/gold); points are redeemed for coupons; coupons are applied at checkout". There is no prior code. Let's apply the method from 05-01 decision by decision:

Decision Forces (what varies / what is stable) Diagnosis Outcome
How does loyalty find out about deliveries? Who listens varies; the order is stable Broadcast to an anonymous interested party, no protocol Observer: PointsAccumulator implements OrderObserver — we subscribe to the existing infrastructure; the order doesn't change a single line
How are points calculated per tier? The per-tier algorithm varies (gold multiplies, silver rounds, bronze is flat) Alternative algorithms chosen from outside Strategy: PointsCalculation with one implementation per tier
How are coupons applied to the total? The discount rule varies; the checkout is stable Same — and the rules already combine (coupon + free delivery) Strategy for the rule + the applicable coupons as an ordered list (see trade-offs)
Where does it hook into checkout? Stable: the facade orchestrates The "apply discounts" step is just one more call One new line in CheckoutFacade, no new pattern
How are coupons created on redemption? Three types today, a new campaign every quarter Redemption knows what it wants, not which class Factory Method with a registry of Suppliers, a replica of the NotifierRegistry
The customer's tier (bronze→silver→gold)? Goes up with points, down with inactivity Transitions with per-stage rules? Yes, but... Wait: today it's just a numeric threshold — enum + per-tier PointsCalculation. Intent note: "if per-tier legal operations appear, evaluate State"

The heart of the module, with the chosen pieces:

// Observer: loyalty subscribes; the rest of the system doesn't even know it exists
public class PointsAccumulator implements OrderObserver {

    private final PointsRepository repository;
    private final Map<Tier, PointsCalculation> calculations; // Strategy per tier

    @Override
    public void orderUpdated(Order order, OrderState previous, OrderState next) {
        if (!(next instanceof DeliveredState)) {
            return;                                   // we only care about delivery
        }
        Customer customer = order.customer();
        PointsCalculation calculation = calculations.get(customer.tier());
        int points = calculation.calculate(order);    // the tier's algorithm, and only that
        repository.credit(customer, points);
    }
}

// Strategy: each tier encapsulates ITS arithmetic; the accumulator doesn't know which one it uses
public class GoldPointsCalculation implements PointsCalculation {
    @Override
    public int calculate(Order order) {
        return order.total().multiply(new BigDecimal("2")).intValue(); // x2 for gold
    }
}

The most valuable decision in the table is the first one: reusing the existing Observer instead of inventing infrastructure. A well-designed new module attaches itself to the seams the system already offers — which is why the seams matter more than the patterns.

The trade-offs: where we decided not to use a pattern

Three temptations rejected, each with its why — because real design is also told through what was not built:

  • Chain of Responsibility for applying coupons in order? Tempting: "each coupon decides whether it applies and passes to the next"... but all applicable coupons must be applied; nobody cuts the chain short. It fails the litmus test from 04-13. A for over an ordered List<DiscountRule> does the same with zero extra classes:
BigDecimal total = subtotal;
for (DiscountRule rule : applicableCoupons) {      // order: biggest discount first
    total = rule.apply(total, cart);                // Strategy yes; Chain wasn't needed
}
  • Interpreter for the redemption rules? ("500 points AND tier >= silver"). RuleExpression from 04-04 already exists for promotions... but the redemption rules today are two, and the team itself writes them in Java — not marketing in text. Without an external author for the mini-language, Interpreter is cost with no customer. An intent note in the code, and move on.
  • A dedicated Facade for loyalty? The module has three public operations (checkPoints, redeem, applyCoupons) over two classes. A facade in front of so little is a monumental doorway for a broom closet — to be revisited if the module grows.

The module's final balance: two Strategies, one reused Observer, one Factory Method with a registry — and three patterns rejected with an argument. That is applying the catalog with judgment: every pattern present has a symptom that claims it, and every absence has an articulated why.

Common Mistakes and Tips

  • Designing the module "by patterns" ("it will need its Singleton, its Factory and its Facade") instead of by decisions. Case B's decision-by-decision table is the antidote: each row has forces and a diagnosis, not a pattern quota.
  • Duplicating infrastructure instead of plugging into the existing seams. Before creating your own event system, chain or factory, check whether the system already exposes one (the PointsAccumulator invented nothing: it subscribed).
  • Thick seams: if the facade starts validating "just this special case" or an observer starts orchestrating, the boundary is coming apart. The facade must stay boring; the observer, reactive.
  • Not recording the rejections. The next developer will see the coupon for loop and think "there's a Chain missing here". A two-line comment with the why of the rejection is worth more than silent elegance.
  • Tip: in design review, always ask for both lists — patterns used and patterns rejected. A design without rejections has almost never been designed: it has been decorated.

Exercises

Exercise 1: seam map

Without looking at the diagram: draw (or describe) the order's journey from case A, naming the six patterns and the five seams (which interface or code point separates each pair). Then answer: if the business asks for "orders above €100 require phone verification before cooking starts", which seam absorbs the change and which patterns never find out?

Exercise 2: extending loyalty with the method

The business expands: "on the customer's birthday, their orders accrue double points, however they combine with their tier". Apply the method: forces, diagnosis, candidate — and careful: there is a solution with a structural pattern from module 3 and a solution with no pattern; present both and choose using the filters.

Exercise 3: spotting the wrong rejection

A colleague implemented coupon application as a real Chain of Responsibility: each coupon extends CouponHandler, applies its discount and calls next.handle(...). It works. Write (a) why formally it is not a Chain even though it looks like one, (b) what practical problem it will cause the day a coupon truly needs to "cut" (an exclusive, non-combinable coupon), (c) the minimal refactoring toward the list-based version.

Solutions

Solution 1: Seams: (1) app–facade: the confirmOrder method (the app sees nothing else); (2) facade–chain: the OrderHandler interface of the first link; (3) facade–strategy: the DeliveryFeeCalculation interface; (4) facade–builder: the fluent API of Order.Builder; (5) order–observers: transitionTo. The phone verification change is a conditional admission rule that can cut the flow short → a new PhoneVerificationHandler link in the chain's assembly (seam 2)... or, if the verification is asynchronous (waiting for the call), a new UNDER_VERIFICATION state in the lifecycle (seam 5, State). Under the synchronous reading: Builder, Strategy, State and Observer never find out; neither does the facade — only the chain's assembly changes.

Solution 2: Forces: what varies is a temporary multiplier that stacks on top of the tier's calculation; stable is the PointsCalculation interface and the accumulator. The pattern option: Decorator — a BirthdayPointsCalculation implements PointsCalculation that wraps the tier's calculation and doubles its result; it combines with any tier without touching it (exactly the dish extras from 03-05). The no-pattern option: an if (customer.isBirthday()) points *= 2; in the accumulator. Filters: if "double points" is the only modifier in sight, the if wins (KISS); if marketing is already talking about "double points on Tuesdays" and "x3 during your first premium week" — modifiers that stack — the axis of variation is real and Decorator earns its cost. The honest answer depends on the roadmap, and saying it that way is the correct answer.

Solution 3: (a) The litmus test: in a Chain each link decides whether to handle or pass and can cut the flow short; here everyone always applies and nobody cuts — it is unconditional delegation, that is, Decorator's structure wearing Chain's name, and the real intent is "apply them all" (a traversal). (b) When the exclusive coupon arrives, cutting the chain will make the result depend invisibly on the assembly order (does it cut before or after the free-delivery coupon?), and the bug will live in configuration, undetectable in the coupons' code. With an explicit list, exclusivity is a visible rule (filter before iterating). (c) Extract each CouponHandler's logic into DiscountRule.apply(total, cart), remove the next field, and replace the chain kickoff with the for over the ordered list — step by step and with tests, as the refactoring lesson prescribes.

Conclusion

You have seen the catalog working at real scale: six patterns collaborating in an order's journey with the facade as the conductor of an orchestra who plays no instrument, and a new module designed decision by decision — with its patterns chosen by symptom, its infrastructure reused through the seams, and its three rejections argued. The underlying lesson: a good design is recognized as much by where its boundaries sit as by which patterns inhabit it. But PideYa is our laboratory; the natural question is whether the software you use every day — the JDK, Spring, Hibernate — is built the same way. It is, and learning to recognize its patterns while reading its code is the next lesson: Design Patterns in Real Projects.

© Copyright 2026. All rights reserved