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
- The problem in PideYa: international expansion
- Intent and structure of the pattern
- Complete Java implementation
- Adding a market vs. adding a product: the key asymmetry
- Relation to and difference from Factory Method
- When to use it and when not to
- 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 addcreateFiscalAddressValidator()to theMarketFactoryinterface... 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 insideCheckoutService, 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
MarketFactorywith 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
TestFactoryreturning 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, notConektaFactory): 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:
- Creating the right
SalesReportobject (PDF, Excel, HTML) according to what the restaurant requests. - In-house fleet couriers and freelance couriers each require, per regime, their
Contract, theirInsurancePolicy, and theirSettlementCalculator, always from the same regime. - Each promotion type (2-for-1, free delivery, percentage discount) needs to create its own
ValidationRuleobject.
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:
- Factory Method (or even a simple factory): a single product (
SalesReport), with no consistency ties to other objects. - 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.
- 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.
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
