Every pattern in this series fixes something. Before we fix anything, let’s meet the thing we’re fixing.

Say hello to CornerStore, a small fictional online shop. It sells running shoes, kitchenware, ebooks and gift cards to customers in Australia and New Zealand. It works. Every order is priced correctly, every refund lands, every report adds up.

And the code is a mess. On purpose.


Agenda:

  1. What CornerStore Does
  2. Why the Code Is Messy on Purpose
  3. A Taste of the Mess
  4. The 23 Smells
  5. The Safety Net
  6. Run It Yourself

What CornerStore Does

CornerStore is a .NET 10 console app with in-memory data. No database, no web host, no authentication. That’s deliberate. I wanted nothing on screen except the code each pattern changes.

Instead of a UI, the store runs a scripted trading day from a JSON file. Over 75 steps, it:

  • browses a catalogue of nested categories, bundles and product variants
  • bulk imports 100,000 product listings
  • registers customers in two regions, AU and NZ
  • builds carts, applies promo codes, picks shipping methods and add-ons, and undoes changes
  • checks out with card, PayPal or Afterpay, books a courier and sends notifications
  • ships, delivers, cancels and refunds orders
  • lets back-office staff adjust stock
  • exports sales reports as CSV and JSON

Here’s how the feature areas fit together:

flowchart LR
    Catalog --> Cart
    Cart --> Pricing
    Pricing --> Checkout
    Checkout --> Payments
    Checkout --> Inventory
    Checkout --> Courier
    Checkout --> Orders
    Orders --> Notifications
    Orders --> Reporting
    BackOffice[Back office] --> Inventory
    BackOffice --> Orders

Every step writes business outcomes to a transcript. Here’s step 60, where Ava from Sydney finally checks out:

[060] checkout customer=cu-1 payment=card
  card charged A$300.33 for Ava
  ShipFast booked SF-0001 for o-1001 (30033 cents)
  email to Ava: order o-1001 confirmed, total A$300.33
  loyalty updated for Ava (Paid)
  analytics order o-1001 Paid
  checkout placed o-1001

Remember that transcript. It becomes very important in a minute.


Why the Code Is Messy on Purpose

If you’ve worked on enough projects, you’ve inherited code like this. Nobody set out to write it. It grew one reasonable change at a time: one more if, one more case, one more field copied by hand.

That’s exactly the shape I wanted. Each smell in CornerStore is a learning seam: a spot where a specific Gang of Four pattern is the natural fix.

Two rules kept it honest:

  1. Wrong in shape, never wrong in behaviour. There are no planted bugs. The mess is in how the code is organised, not in what it calculates.
  2. Realistic, not absurd. No 3,000-line methods or variables named x2. Just the kind of code that passes a quick review and then slowly gets in everyone’s way.

Every smell is marked in the code with a comment, so you can find them:

    // SMELL S10: the console coordinates every checkout subsystem in a fixed order

A Taste of the Mess

The star of the show is StoreConsole.cs. It’s 772 lines in a codebase of about 2,500, and it knows far too much.

Here’s part of its checkout method. Pricing, validation, payment, stock reservation, rollback and courier booking all happen in one place:

        var customer = customers[cart.CustomerId];
        var quote = pricing.BuildQuote(cart, customer);
        var error = orderValidator.Validate(customer, cart, quote);
        if (error is not null)
        {
            Log.Info("checkout rejected: " + error);
            return;
        }

        var paymentType = Text(step, "payment");
        if (!ChargePayment(paymentType, customer, quote.Total))
        {
            Log.Info("checkout rejected: unknown payment type " + paymentType);
            return;
        }

        var order = orderService.Create(customer, cart, quote, paymentType);
        var reservedLines = new List<CartLine>();
        foreach (var line in order.Lines)
        {
            if (!inventory.ReserveStock(line.Product.Sku, line.Quantity))
            {
                foreach (var reservedLine in reservedLines)
                {
                    inventory.AdjustStock(reservedLine.Product.Sku, reservedLine.Quantity);
                }

                Log.Info("checkout rejected: stock changed for " + line.Product.Sku);
                return;
            }

            reservedLines.Add(line);
        }

Count the smells hiding in there. ChargePayment builds payment processors with a switch (that’s Factory Method). The validator is one long nested method (Chain of Responsibility). And the whole sequence belongs somewhere other than the console (Facade).

That’s the theme of this series. Real code rarely has one problem at a time.


The 23 Smells

Here’s the full list, grouped the way the book groups the patterns that fix them.

Creational

Smell What’s wrong Fixed by
S01 Regional rules are scattered across tax, shipping and currency code Abstract Factory
S02 Orders and invoices use eleven-argument constructors Builder
S03 Checkout constructs payment processors in a switch Factory Method
S04 Product variants copy fields by hand Prototype
S05 Configuration and logging are mutable static state Singleton

Structural

Smell What’s wrong Fixed by
S06 Checkout depends directly on the courier’s XML API Adapter
S07 Message type and delivery channel multiply into separate classes Bridge
S08 Bundles follow a separate catalogue and pricing path Composite
S09 Add-ons are boolean flags and pricing branches Decorator
S10 The console coordinates every checkout subsystem Facade
S11 Imported listings duplicate the same product specifications Flyweight
S12 Inventory reads are uncached and access checks are scattered Proxy

Behavioral

Smell What’s wrong Fixed by
S13 Checkout validation is one nested method Chain of Responsibility
S14 Back-office actions run inline with no undo or audit Command
S15 Promo rules are parsed with string operations Interpreter
S16 Three copies of the same recursive tree walk Iterator
S17 Cart widgets update each other directly Mediator
S18 Cart internals are public so undo can copy them Memento
S19 Order transitions call every side-effect service Observer
S20 Order transitions branch on status strings State
S21 Shipping cost is picked with a switch Strategy
S22 CSV and JSON exporters duplicate the same workflow Template Method
S23 Tax, weight and reports switch on line-item types Visitor

The repository’s smells catalog lists the exact files for each smell and the trading-day steps that exercise it.


The Safety Net

Here’s the question every refactor should answer: how do you know you didn’t break anything?

CornerStore’s answer is a golden master test. The baseline’s transcript of the trading day, all 417 lines of it, was approved once and saved to a file. Every one of the 23 pattern solutions, and the capstone, runs the same trading day and must produce that transcript byte for byte.

To make that possible, the app is completely deterministic:

  • time comes from a fixed TimeProvider, never DateTime.Now
  • IDs are sequential, never Guid.NewGuid()
  • the random number generator is seeded from the scenario
  • all formatting uses the invariant culture

That one test does a lot of heavy lifting. It means every “after” in this series is provably equivalent to its “before”. The patterns change the shape of the code, and the transcript proves nothing else changed.

It’s also a technique worth stealing for your own legacy code. Before refactoring something you don’t fully understand, capture what it does today. Then make sure it keeps doing exactly that.


Run It Yourself

From the repository root:

dotnet build baseline/CornerStore.Baseline.slnx
dotnet test baseline/CornerStore.Baseline.slnx
dotnet run --project baseline/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json

Then open baseline/src/CornerStore and search for SMELL. Try to guess which pattern fixes each one before you read the post.

Next up, we start the Creational patterns with Abstract Factory, and the first smell on the list: regional rules scattered all over the place.

Happy Coding!


Previous: Introduction · Next: Abstract Factory