In the previous lesson we learned to manufacture products one at a time without coupling the client to their classes. Today we go up a level: there are problems where objects do not travel alone but in families that must be mutually consistent, and creating a piece from the wrong family is a bug — sometimes a very expensive one. It is exactly what happened to PideYa when it expanded into Mexico: a Mexican payment gateway combined with a Spanish VAT calculator. Abstract Factory exists so that such a mix becomes impossible by construction.

Contents

  1. The problem in PideYa: international expansion
  2. Intent and structure of the pattern
  3. Complete Java implementation
  4. Adding a market vs. adding a product: the key asymmetry
  5. Relation to and difference from Factory Method
  6. When to use it and when not to
  7. Common mistakes, exercises, and conclusion

The problem in PideYa: international expansion

PideYa opens operations in Mexico. Each market needs its own version of three pieces involved in every charge:

Piece Spain Mexico
Payment gateway Redsys (cards, Bizum) Conekta (cards, OXXO)
Tax calculator 10% VAT for food delivery 16% general VAT
Receipt formatter Spanish rules (NIF, "IVA") Mexican rules (RFC, "IVA", CFDI legend)

The three pieces already have their interfaces (PaymentGateway, TaxCalculator, ReceiptFormatter) and the checkout programs against them. The first attempt at internationalization created each piece separately:

// First attempt: each piece is chosen on its own. Do NOT imitate.
PaymentGateway gateway = switch (country) {
    case ES -> new RedsysGateway(config.getRedsysKey());
    case MX -> new ConektaGateway(config.getConektaKey());
};
TaxCalculator taxes = switch (country) {
    case ES -> new SpainTaxes();
    case MX -> new MexicoTaxes();
};
ReceiptFormatter receipt = switch (country) {
    case ES -> new SpainReceipt();
    case MX -> new MexicoReceipt();
};

Besides tripling the switch (a pain we know from the previous lesson), this code has a graver and subtler flaw: the family's consistency depends on human discipline. Nothing prevents a careless refactor, a badly merged branch, or a copy-paste from leaving ConektaGateway living alongside SpainTaxes. That was literally the production incident: charges in Mexico with a 10% Spanish VAT. The compiler did not complain, because each piece was valid on its own; what was invalid was the combination, and no type represented it.

The need, stated precisely: we want to create the whole family at once, guaranteeing that all the pieces belong to the same market, and that the checkout never knows which market it is serving.

Intent and structure of the pattern

Intent (GoF): provide an interface for creating families of related or dependent objects without specifying their concrete classes.

The solution: a factory interface with one creation method per product type in the family. Each implementation of the factory corresponds to a variant (a market) and manufactures all the products of that variant. The client receives one factory and asks it for every piece: consistency is guaranteed because a MexicoFactory is physically incapable of producing Spanish taxes.

classDiagram
    class MarketFactory {
        <<interface>>
        +createPaymentGateway() PaymentGateway
        +createTaxCalculator() TaxCalculator
        +createReceiptFormatter() ReceiptFormatter
    }
    class SpainFactory {
        +createPaymentGateway() PaymentGateway
        +createTaxCalculator() TaxCalculator
        +createReceiptFormatter() ReceiptFormatter
    }
    class MexicoFactory {
        +createPaymentGateway() PaymentGateway
        +createTaxCalculator() TaxCalculator
        +createReceiptFormatter() ReceiptFormatter
    }
    class PaymentGateway { <<interface>> }
    class TaxCalculator { <<interface>> }
    class ReceiptFormatter { <<interface>> }
    class RedsysGateway
    class ConektaGateway
    class SpainTaxes
    class MexicoTaxes
    class SpainReceipt
    class MexicoReceipt
    class CheckoutService

    MarketFactory <|.. SpainFactory
    MarketFactory <|.. MexicoFactory
    PaymentGateway <|.. RedsysGateway
    PaymentGateway <|.. ConektaGateway
    TaxCalculator <|.. SpainTaxes
    TaxCalculator <|.. MexicoTaxes
    ReceiptFormatter <|.. SpainReceipt
    ReceiptFormatter <|.. MexicoReceipt
    SpainFactory ..> RedsysGateway : creates
    SpainFactory ..> SpainTaxes : creates
    SpainFactory ..> SpainReceipt : creates
    MexicoFactory ..> ConektaGateway : creates
    MexicoFactory ..> MexicoTaxes : creates
    MexicoFactory ..> MexicoReceipt : creates
    CheckoutService --> MarketFactory : uses

The GoF roles mapped onto PideYa:

GoF role In PideYa
AbstractFactory MarketFactory
ConcreteFactory (one per variant) SpainFactory, MexicoFactory
AbstractProduct (one per piece type) PaymentGateway, TaxCalculator, ReceiptFormatter
ConcreteProduct RedsysGateway, MexicoTaxes, SpainReceipt, ...
Client CheckoutService (knows only interfaces)

Look at the geometry, which is the pattern's visual signature: a matrix of variants × products. Rows are markets; columns are product types; each concrete factory materializes a complete row. And unlike Factory Method, here the factory is an object that gets passed and injected: object scope, composition over inheritance.

Complete Java implementation

Abstract products (two of the three, for brevity; ReceiptFormatter is analogous):

public interface PaymentGateway {
    PaymentResult charge(BigDecimal amount, PaymentData data);
}

public interface TaxCalculator {
    /** Returns the tax amount for a given taxable base. */
    BigDecimal calculate(BigDecimal taxableBase);
}

Concrete products for each market:

public class RedsysGateway implements PaymentGateway {
    private final String merchantKey;

    public RedsysGateway(String merchantKey) { this.merchantKey = merchantKey; }

    @Override
    public PaymentResult charge(BigDecimal amount, PaymentData data) {
        // Real Redsys integration: signature, virtual POS, Bizum...
        return PaymentResult.accepted("redsys-" + UUID.randomUUID());
    }
}

public class SpainTaxes implements TaxCalculator {
    private static final BigDecimal VAT_FOOD_DELIVERY = new BigDecimal("0.10");

    @Override
    public BigDecimal calculate(BigDecimal base) {
        return base.multiply(VAT_FOOD_DELIVERY).setScale(2, RoundingMode.HALF_UP);
    }
}

public class ConektaGateway implements PaymentGateway {
    private final String apiKey;

    public ConektaGateway(String apiKey) { this.apiKey = apiKey; }

    @Override
    public PaymentResult charge(BigDecimal amount, PaymentData data) {
        // Conekta integration: cards, cash payments via OXXO...
        return PaymentResult.accepted("conekta-" + UUID.randomUUID());
    }
}

public class MexicoTaxes implements TaxCalculator {
    private static final BigDecimal VAT_GENERAL = new BigDecimal("0.16");

    @Override
    public BigDecimal calculate(BigDecimal base) {
        return base.multiply(VAT_GENERAL).setScale(2, RoundingMode.HALF_UP);
    }
}

The abstract factory and the concrete ones — the heart of the pattern:

public interface MarketFactory {
    PaymentGateway createPaymentGateway();
    TaxCalculator createTaxCalculator();
    ReceiptFormatter createReceiptFormatter();
}

public class SpainFactory implements MarketFactory {
    private final PideYaConfig config;

    public SpainFactory(PideYaConfig config) { this.config = config; }

    @Override
    public PaymentGateway createPaymentGateway() {
        return new RedsysGateway(config.getRedsysKey());
    }

    @Override
    public TaxCalculator createTaxCalculator() {
        return new SpainTaxes();
    }

    @Override
    public ReceiptFormatter createReceiptFormatter() {
        return new SpainReceipt();
    }
}

public class MexicoFactory implements MarketFactory {
    private final PideYaConfig config;

    public MexicoFactory(PideYaConfig config) { this.config = config; }

    @Override
    public PaymentGateway createPaymentGateway() {
        return new ConektaGateway(config.getConektaKey());
    }

    @Override
    public TaxCalculator createTaxCalculator() {
        return new MexicoTaxes();
    }

    @Override
    public ReceiptFormatter createReceiptFormatter() {
        return new MexicoReceipt();
    }
}

Notice that each factory method here is a factory method (in the broad sense): Abstract Factory is, in its most common implementation, a bundle of factory methods grouped by variant. Hence the GoF's remark that the two patterns usually show up together.

The client: receives the injected factory and never mentions any market, any concrete class, ever:

public class CheckoutService {

    private final PaymentGateway gateway;
    private final TaxCalculator taxes;
    private final ReceiptFormatter formatter;

    /** The whole family comes from THE SAME factory: consistency by construction. */
    public CheckoutService(MarketFactory factory) {
        this.gateway = factory.createPaymentGateway();
        this.taxes = factory.createTaxCalculator();
        this.formatter = factory.createReceiptFormatter();
    }

    public Receipt confirmOrder(Order order) {
        BigDecimal base = order.getTotalBeforeTaxes();
        BigDecimal tax = taxes.calculate(base);
        BigDecimal total = base.add(tax);

        PaymentResult result = gateway.charge(total, order.getPaymentData());
        if (!result.isAccepted()) {
            throw new PaymentRejectedException(result.getReason());
        }
        return formatter.format(order, base, tax, total);
    }
}

The composition root, the only place where the market gets decided (note that it is the same move we learned with the notifiers: a single decision point, here elevated from "which product" to "which family"):

MarketFactory factory = switch (market) {
    case ES -> new SpainFactory(config);
    case MX -> new MexicoFactory(config);
};
CheckoutService checkout = new CheckoutService(factory);

Now the mix that caused the incident is impossible to write: there is no sequence of calls that combines Conekta with Spanish taxes, because the pieces are no longer chosen one by one. The invariant "everything from the same market" has gone from being a convention to being a property of the type system. And as a bonus: testing the checkout is trivial with a TestFactory that returns consistent doubles.

Adding a market vs. adding a product: the key asymmetry

Every design decision buys flexibility on one axis by paying for it on another. Abstract Factory's trade is crisp and you must know it before adopting the pattern:

  • Adding a variant (a row) is cheap and clean. France: FranceFactory + FranceStripeGateway + FranceTaxes + FranceReceipt. Everything is new classes; neither the client nor the existing factories get touched. Perfect OCP along the "markets" axis.
  • Adding a product type (a column) is expensive. If every market now needs a FiscalAddressValidator, you must add createFiscalAddressValidator() to the MarketFactory interface... and that breaks every existing concrete factory, which must implement it. It is a cascading modification, the pattern's structural price.

Practical rule: Abstract Factory fits when the family's set of products is stable and what grows is the set of variants. If you expect the opposite (new products often, fixed variants), the pattern will make you suffer; consider smaller factories or segregated interfaces (the ISP spirit from the principles lesson).

Relation to and difference from Factory Method

It is the most frequent confusion in the whole module; let's settle it with a table:

Aspect Factory Method Abstract Factory
Manufactures One product A family of related products
Mechanism A method (often overridable) inside a class with its own logic An object dedicated exclusively to manufacturing, with one method per product
GoF scope Class (inheritance decides the product) Object (the factory is injected; composition)
Guarantees consistency across products Not applicable (there is only one) Yes: it is its reason for existing
Dominant intent Open an extension point Lock in a valid combination
Mutual relation — Its methods are usually implemented as factory methods

Two sentences to take home: Factory Method is a method; Abstract Factory is an object. And: if your products do not need to be consistent with one another, you do not have a family: you have loose products, and Factory Method (or several) is enough. We will trace the natural evolution from one to the other in the module comparison.

When to use it and when not to

Use it when:

  • The system must work with several interchangeable families of products and intra-family consistency is a business invariant (PideYa's markets; the classic GoF example: GUI look and feel, with buttons and menus that must all share the same style).
  • You want to be able to swap the entire family at a single point (configuration, startup, even hot-swapped).
  • You want the type system to enforce that "pieces from A do not mix with pieces from B".

Do not use it when:

  • There is only one family (one market): that is the over-engineering cataloged in the lesson on the scale; the day the second market arrives, refactoring toward the pattern will be natural.
  • The "products" have no consistency relationship: they are independent factories dressed up as a family.
  • The family's product catalog changes often (the previous section's asymmetry will punish you).

Relation to other patterns (mention only): concrete factories usually have no state and are often shared as a Singleton; a factory can be implemented internally with Prototype (cloning exemplars instead of instantiating); and the complex products the factory returns may be built internally with a Builder.

Common Mistakes and Tips

  • Leaking the market into the client. If an if (country == MX) shows up inside CheckoutService, the pattern has failed: all per-market variation must live in the concrete products or their factory. The client must speak only interfaces.
  • The "junk drawer" factory. Stuffing MarketFactory with products that have no consistency relationship (createOrderLogger()?) just because "the factory is already at hand". Every new method makes the interface more expensive for every variant; the family must have a clear membership criterion.
  • Silent combinatorial explosion. With 5 markets and 4 products you get 20 concrete classes + 5 factories. That is the pattern's honest cost; if several variants share pieces (Mexico and Colombia use the same gateway), extract base classes or compose them: factories may return shared instances.
  • Forgetting the test factory. A TestFactory returning consistent doubles is one of the pattern's greatest gifts to your tests; if you do not write it, you are paying for the pattern without collecting one of its dividends.
  • Tip: name factories after the variant, not the technology (MexicoFactory, not ConektaFactory): the technology is a detail that can change within the same variant without touching anyone.

Exercises

Exercise 1: add the France market

Add France to the lesson's implementation: Stripe gateway (needs stripeApiKey from configuration), 10% VAT for food delivery, and a receipt following French rules (SIRET). Write the concrete factory and list the modified files.

Exercise 2: spot the broken family

A teammate proposes this "improvement" to save classes. Which guarantee of the pattern does it destroy, and which concrete error becomes possible again?

public class FlexibleFactory implements MarketFactory {
    private final Country gatewayCountry;
    private final Country taxCountry;
    private final Country receiptCountry;

    public FlexibleFactory(Country gatewayCountry, Country taxCountry, Country receiptCountry) { /*...*/ }

    @Override
    public PaymentGateway createPaymentGateway() {
        return gatewayCountry == Country.ES ? new RedsysGateway(...) : new ConektaGateway(...);
    }
    // ... analogous for taxes and receipt, each with ITS OWN country
}

Exercise 3: Factory Method or Abstract Factory?

For each PideYa situation, decide which of the two patterns fits and justify it in one sentence:

  1. Creating the right SalesReport object (PDF, Excel, HTML) according to what the restaurant requests.
  2. In-house fleet couriers and freelance couriers each require, per regime, their Contract, their InsurancePolicy, and their SettlementCalculator, always from the same regime.
  3. Each promotion type (2-for-1, free delivery, percentage discount) needs to create its own ValidationRule object.

Solutions

Solution 1:

public class StripeGateway implements PaymentGateway { /* charge using stripeApiKey */ }
public class FranceTaxes implements TaxCalculator {
    private static final BigDecimal TVA_FOOD = new BigDecimal("0.10");
    @Override
    public BigDecimal calculate(BigDecimal base) {
        return base.multiply(TVA_FOOD).setScale(2, RoundingMode.HALF_UP);
    }
}
public class FranceReceipt implements ReceiptFormatter { /* SIRET, mentions légales */ }

public class FranceFactory implements MarketFactory {
    private final PideYaConfig config;
    public FranceFactory(PideYaConfig config) { this.config = config; }
    @Override public PaymentGateway createPaymentGateway() { return new StripeGateway(config.getStripeApiKey()); }
    @Override public TaxCalculator createTaxCalculator() { return new FranceTaxes(); }
    @Override public ReceiptFormatter createReceiptFormatter() { return new FranceReceipt(); }
}

Modified files: only the composition root (the startup switch gains the FR case). Client, interfaces, and existing factories: untouched.

Solution 2: it destroys the family consistency invariant, which is the pattern's sole reason for existing. By parameterizing each product with its own country, the combination ConektaGateway + SpainTaxes becomes expressible again (just build new FlexibleFactory(MX, ES, MX)): exactly the production incident that motivated the pattern, now with extra ceremony. It is an Abstract Factory in appearance with the safety of three loose switch blocks.

Solution 3:

  1. Factory Method (or even a simple factory): a single product (SalesReport), with no consistency ties to other objects.
  2. Abstract Factory: three products per regime that must be mutually consistent (a freelance contract with fleet insurance would be a legal problem): a textbook family.
  3. Factory Method: each promotion (creator) manufactures its product (ValidationRule); one product per creator, no families.

Conclusion

Abstract Factory elevates the previous lesson's move from the product to the family: an interface with one creation method per piece, one implementation per variant, and the guarantee — by types, not by discipline — that pieces never mix across variants. You have seen its matrix geometry (variants × products), its cost asymmetry (new variant cheap, new product expensive), and the border with Factory Method: a method versus an object, one product versus a consistent family.

The two factory patterns solve which class to instantiate. The next challenge is different: sometimes the class is crystal clear, and what is devilish is the process of assembling it, with ten parameters, half of them optional, and cross-cutting validation rules. It is the story of the telescoping constructor of Order we left pending in the introduction, and it is solved by building step by step: see you in Builder.

© Copyright 2026. All rights reserved