Design patternCreational Patterns
Abstract Factory
Create families of related objects through one interface so client code always gets a matching set and never names the concrete classes.
- Complexity
- Medium
- Seen in the wild
- High
- Also known as
- Kit
Intent
Provide one interface with a creation method for each kind of object in a family, and implement it once per family. Client code asks the factory for its parts and gets a set that is guaranteed to fit together, without ever naming a concrete class.
Forces
- Objects that must match vs code that builds them separately: mixing an EU tax calculator with a US invoice layout is a bug that compiles fine.
- One switch per object vs one decision per family: when every constructor call checks the same
marketflag, the flag leaks everywhere. - Adding a family vs touching every client: a new market should be one new class, not edits across the codebase.
- Adding a product kind vs a stable factory interface: every new method on the factory must be implemented by every family.
Therefore
Group the creation of each family's objects behind one factory interface, pick the concrete factory once at startup, and pass it to the code that needs the products.
The decision "which family?" is made exactly once, at the composition root. Everything downstream receives a factory (or the products it made) and works only with the product interfaces. Each product interface still matters on its own; the factory's job is only to guarantee consistency between them.
Structure
Diagram source (Mermaid)
classDiagram
class InvoicingKit {
<<interface>>
+taxCalculator() TaxCalculator
+formatter() InvoiceFormatter
}
class EuInvoicing {
+taxCalculator() TaxCalculator
+formatter() InvoiceFormatter
}
class UsInvoicing {
+taxCalculator() TaxCalculator
+formatter() InvoiceFormatter
}
class TaxCalculator {
<<interface>>
+taxCents(net, region) int
}
class VatCalculator {
+taxCents(net, region) int
}
class SalesTaxCalculator {
+taxCents(net, region) int
}
class InvoiceFormatter {
<<interface>>
+render(seq, net, tax) string
}
class Billing {
-TaxCalculator tax
-InvoiceFormatter fmt
+issue(netCents, region) string
}
InvoicingKit <|.. EuInvoicing
InvoicingKit <|.. UsInvoicing
TaxCalculator <|.. VatCalculator
TaxCalculator <|.. SalesTaxCalculator
EuInvoicing ..> VatCalculator : creates
UsInvoicing ..> SalesTaxCalculator : creates
Billing ..> InvoicingKit : uses
Billing --> InvoiceFormatterParticipants
- Abstract factory (
InvoicingKit): one creation method per product kind. - Concrete factories (
EuInvoicing,UsInvoicing): one per family; each returns products that belong together. - Abstract products (
TaxCalculator,InvoiceFormatter): the interfaces clients use. - Concrete products (
VatCalculator,SalesTaxCalculator, and the two formatters): one implementation per family per kind. In the example both calculators are oneRateTabletype configured with different rates; a product only needs its own class when its behavior differs. - Client (
Billing): receives a factory, asks for products, and never sees a concrete class.
Use it when
- Several objects vary together along one axis (market, cloud provider, database vendor, test vs production), and mixing them is a bug.
- The same
if (market === "eu")check appears next to several different constructor calls. - You ship a library that must run against several backends, and each backend needs a matching set of adapters.
- Tests need to swap a whole set of collaborators (in-memory store, fake clock, fake queue) in one move.
Do not use it when
- You have one family today and only imagine a second. That is speculative generality. Use plain constructors and extract the factory when the second family arrives with real requirements.
- Only one product kind varies. Use Factory Method, or pass that one object in.
- The products don't actually constrain each other. Independent choices deserve independent parameters, not a factory that bundles them.
- A dependency injection container or a composition-root function already wires the right implementations per environment. A hand-written factory on top duplicates that wiring.
- Product kinds change often. Every new creation method must be added to every concrete factory, which turns a small feature into a wide change.
Application grill
Inspect first: the concrete classes that would form each family, and how callers choose among them today (constructor calls at the use site, a config value, an environment variable); whether a module or DI container already centralizes construction.
Missing pieces: none beyond the language: one factory module with one function per family. A DI container (Awilix, tsyringe, Spring, Google Wire) earns its place only when wiring is repeated across many entry points; do not introduce one for a single family.
Ask before you build:
- Scale: how many real families exist, and is a third actually planned (a second cloud, a second database vendor)? Two families with no third planned is a conditional, not a factory.
- Performance: is construction on a per-request path where the indirection must stay allocation-cheap?
- Reliability: must an unknown family fail at startup (validate the factory map) or at first use?
- Budget: who maintains the wiring when a family gains or loses a member?
- Implementation: how do tests obtain a family - through the factory like production, or by constructing test doubles directly?
Implementation steps
- List the product kinds and the families. Write them as a grid; every cell must be a real class you need, or the grid is speculative.
- Extract an interface for each product kind from what clients actually call.
- Declare the abstract factory with one creation method per product kind, returning the product interfaces.
- Implement one concrete factory per family. Keep family-specific configuration (rates, prefixes) inside it.
- Choose the concrete factory once, at the composition root, from configuration.
- Change clients to take the factory (or the products) through their constructor, and delete the scattered family checks.
Example
A billing service sells in the EU and the US. EU invoices need VAT by country and EU-style numbering; US invoices need state sales tax and a different layout. Using the EU calculator with the US layout must be impossible.
interface TaxCalculator {
taxCents(netCents: number, region: string): number;
}
interface InvoiceFormatter {
render(seq: number, netCents: number, taxCents: number): string;
}
interface InvoicingKit {
taxCalculator(): TaxCalculator;
formatter(): InvoiceFormatter;
}
const amount = (cents: number) => (cents / 100).toFixed(2);
function rateFor(rates: Record<string, number>, region: string): number {
const rate = rates[region];
if (rate === undefined) throw new Error(`no tax rate for ${region}`);
return rate;
}
class EuInvoicing implements InvoicingKit {
taxCalculator(): TaxCalculator {
const vat = { DE: 0.19, FR: 0.2, IE: 0.23 };
return { taxCents: (net, country) => Math.round(net * rateFor(vat, country)) };
}
formatter(): InvoiceFormatter {
return {
render: (seq, net, tax) =>
`EU-${String(seq).padStart(6, "0")} net ${amount(net)} EUR, VAT ${amount(tax)} EUR, gross ${amount(net + tax)} EUR`,
};
}
}
class UsInvoicing implements InvoicingKit {
taxCalculator(): TaxCalculator {
const salesTax = { CA: 0.0725, NY: 0.04, TX: 0.0625 };
return { taxCents: (net, state) => Math.round(net * rateFor(salesTax, state)) };
}
formatter(): InvoiceFormatter {
return {
render: (seq, net, tax) =>
`Invoice #${seq}: subtotal $${amount(net)}, sales tax $${amount(tax)}, total $${amount(net + tax)}`,
};
}
}
class Billing {
private readonly tax: TaxCalculator;
private readonly fmt: InvoiceFormatter;
private seq = 0;
constructor(kit: InvoicingKit) {
this.tax = kit.taxCalculator();
this.fmt = kit.formatter();
}
issue(netCents: number, region: string): string {
return this.fmt.render(++this.seq, netCents, this.tax.taxCents(netCents, region));
}
}
// Composition root: the family is chosen once.
const kits: Record<string, InvoicingKit> = { eu: new EuInvoicing(), us: new UsInvoicing() };
console.log(new Billing(kits["eu"]).issue(12000, "DE"));
// EU-000001 net 120.00 EUR, VAT 22.80 EUR, gross 142.80 EUR
console.log(new Billing(kits["us"]).issue(12000, "CA"));
// Invoice #1: subtotal $120.00, sales tax $8.70, total $128.70Trade-offs
Gains
- Products from different families can't be mixed by accident.
- The family decision lives in one place, and clients depend only on interfaces.
- A new family is one new factory plus its products, with no client changes.
- Tests can swap an entire environment by passing a different factory.
Costs
- A new product kind changes the factory interface and every implementation.
- Many small types: factories times products. With one family, all of it is overhead.
- An extra layer of indirection between the client and the object it uses.
Relations
- Factory Method creates one product. An abstract factory is often a bundle of factory methods, one per product kind.
- Builder assembles one complex object step by step. Abstract Factory returns finished, related objects immediately.
- Prototype can implement a concrete factory: store one configured instance per product and clone it, instead of writing a subclass per family.
- Singleton is often (and usually needlessly) applied to concrete factories. A factory with no state can be a plain module-level value or injected once.
- Bridge can use an abstract factory to create and configure the implementation side that matches an abstraction.
- Facade can replace an abstract factory when clients only need to hide how a subsystem's objects are created, not swap families.
Agent checklist
- At least two families exist in code today, each with a real implementation of every product kind.
- Mixing products across families is a genuine bug, not just a style preference.
- The concrete factory is chosen exactly once, at the composition root, not inside business logic.
- Clients reference only product and factory interfaces. Search for leftover concrete class names and family flags.
- You checked whether the existing DI setup already covers this wiring before adding a factory layer.
See also
- Factory MethodLet a subclass or a supplied function decide which concrete object to create, so shared workflow code depends only on an interface.
- BuilderAssemble a complex object through named, step-by-step calls and validate it once in build(), instead of a constructor with many parameters.
- PrototypeCreate new objects by copying a configured existing instance, so callers get a ready-made variant without knowing its class or setup.
- SingletonGuarantee one instance of a class per process with a global access point. Usually better replaced by one instance created at startup and injected.
- BridgeSplit a class that varies along two dimensions into two hierarchies joined by composition, so each side grows without multiplying subclasses.
- FacadeGive a complex subsystem one small entry point for a common task, so callers stop orchestrating its classes and calling them in the right order.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.