CornerStore sells to Australia and New Zealand, and every region has its own tax rate, shipping surcharge and currency symbol. The baseline asks “which region is this?” in three different files. Abstract Factory asks once, and hands back everything that belongs together.

   
Category Creational
Smell fixed S01: Regional rules are scattered
Code patterns/creational/abstract-factory

The Scene

Step 40 of the trading day. Ben from New Zealand asks for a quote:

[040] quote customer=cu-2
  quote cu-2 (NZ, standard, 2.10 kg)
  subtotal NZ$116.74
  discount -NZ$5.84 (NZFRIEND)
  add-ons NZ$0.00
  shipping NZ$12.20
  tax NZ$13.07
  total NZ$136.17

Three regional decisions went into that quote: the 15% GST, the NZ$3.00 shipping surcharge, and the NZ$ in front of every number. All three are correct.

Now imagine the business says “we’re launching in a new region next month.” How many places do you need to change? And what stops someone from updating the tax rate but forgetting the currency symbol?


The Smell

In the baseline, each regional rule lives in its own static class with its own branch.

Tax, in TaxCalculator.cs:

if (region == "AU")
{
    return 0.10m;
}
else if (region == "NZ")
{
    return 0.15m;
}

Shipping surcharge, in ShippingCalculator.cs:

        // SMELL S01: the regional shipping surcharge is picked with its own region branch
        if (region == "AU")
        {
            return 0m;
        }
        else if (region == "NZ")
        {
            return 3.00m;
        }

And the currency symbol, in MoneyFormatter.cs:

        // SMELL S01: the currency symbol is picked with its own region branch
        if (region == "AU")
        {
            return "A$" + number;
        }
        else if (region == "NZ")
        {
            return "NZ$" + number;
        }

Each one works on its own. The problem is what isn’t there: nothing ties them together. These three decisions belong to the same region, but the code treats them as unrelated choices. Add a region and you have to remember every branch. Miss one and you get a perfectly valid-looking quote with Australian tax and New Zealand dollars.


The Idea

Abstract Factory gives you one object that creates a whole family of related things. You pick the family once, and everything it creates is guaranteed to match.

Think of buying furniture from a single catalogue range. You pick “Scandinavian” once, and the table, chairs and shelves all come in the same style. You never accidentally end up with one Scandinavian chair and one Victorian chair, because you never chose them separately.

In CornerStore, the “range” is the region. The factory creates the region’s pricing rules and its money formatter. Pick New Zealand once, and everything matches.


The Refactor

First, the factory contract. One factory, two products:

internal interface IRegionFactory
{
    IRegionalPricing CreatePricing();

    IMoneyFormatter CreateMoneyFormatter();
}

Then one concrete factory per region. Here’s New Zealand, with everything about the region in one place:

internal sealed class NewZealandRegionFactory : IRegionFactory
{
    public IRegionalPricing CreatePricing()
    {
        return new RegionalPricing(0.15m, 3.00m);
    }

    public IMoneyFormatter CreateMoneyFormatter()
    {
        return new RegionalMoneyFormatter("NZ$");
    }
}

The pricing service now asks for the family once and reads both the surcharge and the tax rate from the same product:

var regionalPricing = RegionFactoryProvider.For(customer.Region).CreatePricing();
var shipping = methodCost + regionalPricing.ShippingSurcharge;

var rate = regionalPricing.TaxRate;

You might have noticed something. There’s still a switch, in RegionFactoryProvider:

        return region switch
        {
            "AU" => new AustraliaRegionFactory(),
            "NZ" => new NewZealandRegionFactory(),
            _ => new DefaultRegionFactory(),
        };

That’s fine, and it’s an important point. Patterns rarely make decisions disappear. They move them to one place. Before, the region question was asked three times in three files. Now it’s asked once, and the answer is a complete, consistent family.

Here’s the full shape:

classDiagram
    class IRegionFactory {
        <<interface>>
        +CreatePricing() IRegionalPricing
        +CreateMoneyFormatter() IMoneyFormatter
    }
    class IRegionalPricing {
        <<interface>>
        +TaxRate decimal
        +ShippingSurcharge decimal
    }
    class IMoneyFormatter {
        <<interface>>
        +Format(amount) string
    }
    class AustraliaRegionFactory
    class NewZealandRegionFactory
    class DefaultRegionFactory
    class RegionalPricing
    class RegionalMoneyFormatter
    class RegionFactoryProvider
    class PricingService
    class MoneyFormatter

    IRegionFactory <|.. AustraliaRegionFactory
    IRegionFactory <|.. NewZealandRegionFactory
    IRegionFactory <|.. DefaultRegionFactory
    IRegionalPricing <|.. RegionalPricing
    IMoneyFormatter <|.. RegionalMoneyFormatter
    RegionFactoryProvider ..> IRegionFactory : selects
    IRegionFactory ..> IRegionalPricing : creates
    IRegionFactory ..> IMoneyFormatter : creates
    PricingService --> IRegionalPricing : uses
    MoneyFormatter ..> RegionFactoryProvider : selects

Adding a region is now one new factory class and one line in the provider. You can’t add half a region anymore.

One more thing worth noticing: TaxCalculator still has a different switch, on line-item types (physical, digital, gift card). That’s smell S23, and it belongs to Visitor. One pattern, one seam.


What It Cost

This one is not cheap. The refactor touched 15 files: about 134 lines added and 32 removed. ShippingCalculator.cs was deleted entirely.

For two regions, we now have an abstract factory, three concrete factories (including a default for unknown regions), two product interfaces, two products and a provider. That’s a lot of structure for two tax rates.

And the transcript? Identical to the baseline, all 417 lines.


When Not to Use It

  • Only one product varies. If only the tax rate changed by region, a dictionary or a single strategy would do. Abstract Factory earns its keep when several things must change together.
  • Variations won’t grow. If you’ll only ever have two regions and the rules rarely change, the original branches are easier to read.
  • The family is known at startup. If the region is fixed for the whole app (say, one deployment per country), dependency injection can register the right implementations once, without a runtime factory.

Easily Confused With

Factory Method. Both create objects without naming the concrete class at the call site. The difference is scope: Factory Method lets a subclass decide how to create one product, while Abstract Factory creates a family of products that must match each other. See Factory Method vs Abstract Factory for a side-by-side using CornerStore’s payment processors.


Try It Yourself

dotnet build patterns/creational/abstract-factory/AbstractFactory.slnx
dotnet test patterns/creational/abstract-factory/AbstractFactory.slnx
dotnet run --project patterns/creational/abstract-factory/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json

A small exercise: add a SingaporeRegionFactory with a 9% tax rate and an S$ symbol. Notice how few files you touch, and how impossible it is to forget the currency.

For the participant table and a sequence diagram, see the Abstract Factory README. The full change is in DIFF.md.


Previous: Meet CornerStore · Next: Builder