Abstract Factory Pattern: Family of Related Objects
You're building a cross-platform application. Your UI needs buttons, checkboxes, and dialogs. On Windows, these components should look and behave like native Windows widgets. On macOS, they should...
Key Insights
- Abstract Factory excels when your system must create families of related objects that must work together—using mismatched components would cause runtime failures or inconsistent behavior
- The pattern trades flexibility for consistency: adding new product families is easy, but adding new product types requires modifying every factory in your hierarchy
- Abstract Factory naturally composes multiple Factory Methods, making it the right choice when object creation involves coordinating several related types rather than just one
The Problem of Object Families
You’re building a cross-platform application. Your UI needs buttons, checkboxes, and dialogs. On Windows, these components should look and behave like native Windows widgets. On macOS, they should feel like native Mac controls. The catch? Mixing a Windows button with a Mac dialog creates visual chaos and potential runtime errors.
Simple factory methods fall short here. A createButton() method can return platform-specific buttons, but nothing enforces that your button, checkbox, and dialog all come from the same platform. You could accidentally create a Windows button alongside a Mac checkbox. The compiler won’t complain, but your users will.
This is where Abstract Factory shines. It guarantees that related objects—entire families of them—get created together, maintaining consistency across your object graph.
Pattern Structure and Components
Abstract Factory involves four key participants:
AbstractFactory declares the interface for creating each type of product. It’s the contract that all concrete factories must fulfill.
ConcreteFactory implements the creation methods for a specific product family. Each concrete factory produces objects that belong together.
AbstractProduct defines the interface for a type of product. All variants of that product implement this interface.
ConcreteProduct is the actual implementation, created by a specific concrete factory.
The structure forms a grid: factories on one axis, product types on the other. Each cell contains a concrete product class.
Let’s define our cross-platform UI abstractions:
// Abstract Products
interface Button {
render(): void;
onClick(handler: () => void): void;
}
interface Checkbox {
render(): void;
toggle(): void;
isChecked(): boolean;
}
interface Dialog {
setTitle(title: string): void;
setContent(content: string): void;
show(): void;
close(): void;
}
// Abstract Factory
interface UIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
createDialog(): Dialog;
}
These interfaces establish the contract. Any code working with UIFactory doesn’t know or care which platform it’s targeting. It requests components, uses them through their abstract interfaces, and trusts that everything works together.
Implementation Walkthrough
Now we implement concrete factories that produce platform-specific components:
// Windows Concrete Products
class WindowsButton implements Button {
render(): void {
console.log("Rendering Windows-style button with sharp corners");
}
onClick(handler: () => void): void {
// Windows-specific event binding
handler();
}
}
class WindowsCheckbox implements Checkbox {
private checked = false;
render(): void {
console.log("Rendering Windows checkbox with square indicator");
}
toggle(): void {
this.checked = !this.checked;
}
isChecked(): boolean {
return this.checked;
}
}
class WindowsDialog implements Dialog {
private title = "";
private content = "";
setTitle(title: string): void {
this.title = title;
}
setContent(content: string): void {
this.content = content;
}
show(): void {
console.log(`Windows dialog: [${this.title}] ${this.content}`);
}
close(): void {
console.log("Closing Windows dialog with fade animation");
}
}
// Windows Factory
class WindowsUIFactory implements UIFactory {
createButton(): Button {
return new WindowsButton();
}
createCheckbox(): Checkbox {
return new WindowsCheckbox();
}
createDialog(): Dialog {
return new WindowsDialog();
}
}
// Mac Concrete Products
class MacButton implements Button {
render(): void {
console.log("Rendering Mac-style button with rounded corners");
}
onClick(handler: () => void): void {
// Mac-specific event binding
handler();
}
}
class MacCheckbox implements Checkbox {
private checked = false;
render(): void {
console.log("Rendering Mac checkbox with circular indicator");
}
toggle(): void {
this.checked = !this.checked;
}
isChecked(): boolean {
return this.checked;
}
}
class MacDialog implements Dialog {
private title = "";
private content = "";
setTitle(title: string): void {
this.title = title;
}
setContent(content: string): void {
this.content = content;
}
show(): void {
console.log(`Mac sheet dialog: ${this.title} - ${this.content}`);
}
close(): void {
console.log("Closing Mac dialog with slide-up animation");
}
}
// Mac Factory
class MacUIFactory implements UIFactory {
createButton(): Button {
return new MacButton();
}
createCheckbox(): Checkbox {
return new MacCheckbox();
}
createDialog(): Dialog {
return new MacDialog();
}
}
Client code receives a factory and uses it without knowing the concrete type:
function buildSettingsPanel(factory: UIFactory): void {
const saveButton = factory.createButton();
const darkModeCheckbox = factory.createCheckbox();
const confirmDialog = factory.createDialog();
saveButton.render();
darkModeCheckbox.render();
saveButton.onClick(() => {
confirmDialog.setTitle("Save Settings");
confirmDialog.setContent("Your preferences have been saved.");
confirmDialog.show();
});
}
// At application startup
const factory = detectPlatform() === "windows"
? new WindowsUIFactory()
: new MacUIFactory();
buildSettingsPanel(factory);
The buildSettingsPanel function is completely decoupled from platform specifics. Swap the factory, and the entire UI family changes.
Abstract Factory vs Factory Method
These patterns often get confused. Here’s the distinction:
Factory Method defines a single method for creating one type of object. Subclasses override it to change the product type.
Abstract Factory defines multiple creation methods for a family of related objects. It often uses Factory Methods internally.
// Factory Method: Single product focus
abstract class DocumentCreator {
abstract createDocument(): Document;
openDocument(): void {
const doc = this.createDocument();
doc.open();
}
}
class PDFCreator extends DocumentCreator {
createDocument(): Document {
return new PDFDocument();
}
}
// Abstract Factory: Product family focus
interface DocumentSuiteFactory {
createDocument(): Document;
createSpreadsheet(): Spreadsheet;
createPresentation(): Presentation;
}
class MicrosoftSuiteFactory implements DocumentSuiteFactory {
createDocument(): Document {
return new WordDocument();
}
createSpreadsheet(): Spreadsheet {
return new ExcelSpreadsheet();
}
createPresentation(): Presentation {
return new PowerPointPresentation();
}
}
Use Factory Method when you have one product type with variants. Use Abstract Factory when you have multiple product types that must be used together consistently.
Real-World Applications
Database abstraction layers are a classic use case. Different databases require different connection, command, and transaction implementations, but they must be compatible:
interface DbConnection {
connect(connectionString: string): Promise<void>;
disconnect(): Promise<void>;
}
interface DbCommand {
execute(sql: string): Promise<ResultSet>;
executeNonQuery(sql: string): Promise<number>;
}
interface DbTransaction {
begin(): Promise<void>;
commit(): Promise<void>;
rollback(): Promise<void>;
}
interface DatabaseFactory {
createConnection(): DbConnection;
createCommand(connection: DbConnection): DbCommand;
createTransaction(connection: DbConnection): DbTransaction;
}
class PostgresFactory implements DatabaseFactory {
createConnection(): DbConnection {
return new PostgresConnection();
}
createCommand(connection: DbConnection): DbCommand {
return new PostgresCommand(connection as PostgresConnection);
}
createTransaction(connection: DbConnection): DbTransaction {
return new PostgresTransaction(connection as PostgresConnection);
}
}
class MySQLFactory implements DatabaseFactory {
createConnection(): DbConnection {
return new MySQLConnection();
}
createCommand(connection: DbConnection): DbCommand {
return new MySQLCommand(connection as MySQLConnection);
}
createTransaction(connection: DbConnection): DbTransaction {
return new MySQLTransaction(connection as MySQLConnection);
}
}
Other practical applications include theming systems (light/dark mode with consistent colors, fonts, and spacing), game development (different asset families for various game levels or difficulty settings), and dependency injection containers (which are essentially sophisticated abstract factories).
Trade-offs and Considerations
Benefits:
- Guarantees consistency across related objects
- Isolates concrete classes from client code
- Makes exchanging product families trivial
- Promotes the Single Responsibility Principle—each factory handles one family
Drawbacks:
- Adding new product types is painful. Want to add a
Sliderwidget? You must modify the abstract factory interface and every concrete factory. - Can lead to class explosion with many products and families
- Increases initial complexity for simple scenarios
The Open/Closed Principle creates tension here. The pattern is open for new families (just add a new factory) but closed for new product types (requires modifying existing code). Design your product interfaces carefully upfront.
Consider using Abstract Factory when:
- Your system needs multiple families of related products
- Products within a family must be used together
- You want to enforce consistency at compile time
- You need to swap entire product families at runtime
Avoid it when:
- You have only one product type (use Factory Method instead)
- Products don’t need to be used together
- Your product lineup changes frequently
- The added abstraction doesn’t justify the complexity
Summary and Guidelines
Abstract Factory solves a specific problem: creating families of objects that must work together. It’s not a general-purpose creation pattern—it’s a consistency enforcement mechanism.
Use Abstract Factory when:
- Mixing objects from different families would cause errors
- You need to swap entire families at runtime or configuration time
- You’re building platform-agnostic code with platform-specific implementations
Avoid these anti-patterns:
- Creating factories with unrelated products (they should form a cohesive family)
- Using Abstract Factory for a single product type
- Ignoring the cost of adding new product types to existing factories
When implemented correctly, Abstract Factory makes your code more maintainable by centralizing family-specific logic and making platform or variant switching a single-line change. When misapplied, it adds layers of abstraction that obscure rather than clarify. Choose deliberately.