Abstract factory pattern
Abstract Factory is a creational design pattern that provides an interface for creating families of related objects without specifying their concrete classes.
Theory
TL;DR
- Like a car factory blueprint that produces matching sets: engine + tires + seats, all fitting together. Swap the blueprint, get a different but still consistent set.
- Main difference from Factory Method: Abstract Factory creates a whole family of products, Factory Method creates one.
- Use when your app needs swappable product families: UI themes, OS-specific widgets, dev/prod database drivers.
- One factory = one consistent family. Client code never accidentally mixes Windows buttons with Mac checkboxes.
Quick example
interface Button { render(): void; }
interface Checkbox { render(): void; }
interface GUIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
class WindowsFactory implements GUIFactory {
createButton(): Button { return { render: () => console.log('WindowsButton') }; }
createCheckbox(): Checkbox { return { render: () => console.log('WindowsCheckbox') }; }
}
// Pick one factory, get a consistent family
const factory: GUIFactory = new WindowsFactory();
factory.createButton().render(); // WindowsButton
factory.createCheckbox().render(); // WindowsCheckboxOne factory call locks you into a consistent family. No accidental mixing of parts from different families.
Key difference from Factory Method
Factory Method is one method that creates one product, typically overridden in a subclass. Abstract Factory is an interface with multiple methods, each producing a different product from the same family. Client code picks a factory once and all subsequent product calls stay consistent. That consistency is the whole point.
When to use
- Multiple products that must match each other: light/dark UI themes where button, input, and modal all need the same style.
- Runtime config swaps the family: dev uses an in-memory database driver, prod uses PostgreSQL. One factory swap changes all related objects.
- Framework-level abstraction: OS-specific widgets. Java AWT does exactly this with
Toolkit.getDefaultToolkit(). - If you only have one product variant, Factory Method is enough. Abstract Factory adds classes; make sure the tradeoff is worth it.
How the runtime handles it
In TypeScript and Java, the factory reference is abstract at compile time. The runtime resolves actual method calls through vtables, dispatching to the concrete factory based on which instance you assigned. No compile-time binding to concrete product classes. Swap the factory instance via config or dependency injection and all products change without touching client code.
Common mistakes
Treating it like a single-product factory
// Wrong: separate factories per product risk mixing families
class ButtonFactory { createButton(): Button { /* windows */ } }
class CheckboxFactory { createCheckbox(): Checkbox { /* mac */ } }
// Nothing stops mixing Windows button with Mac checkboxGroup all family members under one factory. The family constraint only holds when all products come from the same place.
Instantiating concrete classes in client code
// Wrong: client knows the concrete class
const btn = new WindowsButton(); // Breaks the abstraction entirely
// Right: always through the factory
const btn = factory.createButton();The moment client code references WindowsButton, swapping families requires touching that code. The whole point of the pattern is avoiding this.
Over-abstracting small apps
Ten classes for two buttons is a YAGNI violation. Abstract Factory pays off when you have two or more families with two or more products each. For a single toggle between variants, a simple conditional works fine and is far easier to read.
Singleton factory caching stale state
A module-level singleton factory can cache the wrong family after a config reload or hot restart. I saw this bite a team in Node.js: one worker's theme factory cached a dark theme, and it leaked into subsequent requests after a config change. Fresh instance per request or context fixed it. If your factory carries config state, don't create it once at startup and share it everywhere.
Real-world usage
- Java AWT:
Toolkit.getDefaultToolkit()returns an OS-family factory that createsWindowsButton,MotifMenu, and similar widgets without client code knowing which platform it runs on. - React Native: platform-specific UI kits serve
iOSButtonandAndroidButtonfrom the same interface. - Kubernetes client: factory pattern for cluster configs, in-cluster vs out-of-cluster, so the same app code works inside and outside a cluster.
- gRPC: channel factories switch between HTTP/2 and Unix socket families depending on the deployment context.
Follow-up questions
Q: What is the difference between Abstract Factory and Factory Method?
A: Factory Method is one method that creates one product, usually overridden in a subclass. Abstract Factory is an interface with multiple methods, each creating a different product from the same family. Factory Method = one product. Abstract Factory = one family.
Q: When does Abstract Factory hurt performance?
A: Factory instantiation itself is cheap. The issue is creating a new factory instance on every call instead of reusing it. Cache the factory as a singleton where the family does not change per request.
Q: How is Abstract Factory different from the Prototype pattern?
A: Prototype clones existing objects. Abstract Factory creates fresh instances through factory methods. Prototype is useful when creation is expensive and new objects are similar to existing ones. Abstract Factory is about family consistency, not cloning.
Q: Can Abstract Factory cause circular dependency issues?
A: Yes. If factory A creates object B, and B internally tries to create something back through A, you get a cycle. The fix is to let the factory own the full lifecycle and sequence creation internally. The pattern where createDatabase() calls this.createLogger() inside the factory is the safe approach: factory sequences creation, products do not hold a reference back to the factory.
Q: How do you evolve Abstract Factory to support plugins without changing client code?
A: Use a factory registry with dynamic loading. In Java, ServiceLoader discovers factory implementations at runtime. Client calls AbstractFactory.getInstance(config), and the registry resolves which concrete factory to load. Adding a plugin means dropping in a new factory class with no client code changes. "Add an if/else" is the junior answer. The senior answer is a registry with dynamic resolution.
Examples
Basic: GUI factory for Windows and Mac
interface Button { render(): void; }
interface Checkbox { toggle(): void; }
interface GUIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
class MacFactory implements GUIFactory {
createButton(): Button { return { render: () => console.log('Mac button') }; }
createCheckbox(): Checkbox { return { toggle: () => console.log('Mac checkbox') }; }
}
class WinFactory implements GUIFactory {
createButton(): Button { return { render: () => console.log('Win button') }; }
createCheckbox(): Checkbox { return { toggle: () => console.log('Win checkbox') }; }
}
function renderUI(factory: GUIFactory) {
factory.createButton().render();
factory.createCheckbox().toggle();
}
renderUI(new MacFactory()); // Mac button, Mac checkbox
renderUI(new WinFactory()); // Win button, Win checkboxrenderUI does not know or care which platform it targets. Swap the factory, swap the entire UI family. This is the pattern at its simplest.
Intermediate: React theme factory with OS preference detection
interface ThemeButton { render(): JSX.Element; }
interface ThemeInput { render(): JSX.Element; }
interface ThemeFactory {
createButton(): ThemeButton;
createInput(): ThemeInput;
}
class DarkThemeFactory implements ThemeFactory {
createButton(): ThemeButton {
return { render: () => <button style={{ background: 'black', color: 'white' }}>Click</button> };
}
createInput(): ThemeInput {
return { render: () => <input style={{ background: '#333', color: 'white' }} /> };
}
}
class LightThemeFactory implements ThemeFactory {
createButton(): ThemeButton {
return { render: () => <button style={{ background: 'white', color: 'black' }}>Click</button> };
}
createInput(): ThemeInput {
return { render: () => <input style={{ background: '#fff', color: 'black' }} /> };
}
}
const App: React.FC = () => {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const factory: ThemeFactory = isDark ? new DarkThemeFactory() : new LightThemeFactory();
return (
<div>
{factory.createButton().render()}
{factory.createInput().render()}
</div>
);
};Factory choice happens once at component initialization. All themed elements come from the same factory, so they always match. No manual coordination required.
Advanced: Dependency injection with lifecycle control
interface Logger { log(msg: string): void; }
interface Database { query(sql: string): string; }
interface AppFactory {
createLogger(): Logger;
createDatabase(): Database;
}
class ProdFactory implements AppFactory {
createLogger(): Logger {
return { log: (msg) => console.log(`[PROD] ${msg}`) };
}
createDatabase(): Database {
const logger = this.createLogger(); // Factory sequences creation internally
return {
query: (sql) => {
logger.log(`Executing: ${sql}`);
return 'results';
}
};
}
}
const factory = new ProdFactory();
const db = factory.createDatabase();
db.query('SELECT *'); // [PROD] Executing: SELECT *Database needs Logger, but there is no circular dependency because the factory controls the sequence. If you wired these manually outside the factory, swapping ProdFactory for a DevFactory would require touching all the wiring code. This is where Abstract Factory earns its place in real projects.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.