Skip to main content

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

typescript
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(); // WindowsCheckbox

One 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

typescript
// 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 checkbox

Group 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

typescript
// 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 creates WindowsButton, MotifMenu, and similar widgets without client code knowing which platform it runs on.
  • React Native: platform-specific UI kits serve iOSButton and AndroidButton from 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

typescript
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 checkbox

renderUI 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

typescript
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

typescript
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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?