Definition
An abstract factory is one interface for creating a family of related products, so a client gets products that belong together without naming their concrete types.
After Gamma et al., Design Patterns (1994), Abstract Factory
01When both products must agree
The first solution can be simple.
With one export mode, constructing a screen renderer and a screen stylesheet directly is easy to follow. The caller knows exactly what it needs. Even one small mode check can be perfectly reasonable. Here is that check, a fragment of the editor’s export:
// The editor's export creates the page and the stylesheet it needs, together.
const page = mode === 'print' ? makePage('print-page') : makePage('screen-page');
const styles =
mode === 'print' ? makeStyles('print-page', 'serif') : makeStyles('screen-page', 'sans-serif');
const html = `<style>${styles.css()}</style>${page.render(title)}`; It keeps the pair together: one decision makes both products. The pressure appears when an editor, a scheduled report, and a preview all need screen or print output. If each caller chooses the renderer and stylesheet separately, one update can be missed, and every caller has to know which renderer goes with which stylesheet.
Abstract Factory gives a client one contract for creating several kinds of related products, with a concrete family for each supported choice. Here the product roles are a page renderer and a stylesheet. Screen and print are the families. The caller chooses the family; the export client asks it for both products.
| Concrete family | Its page renderer emits the class | Its stylesheet targets |
|---|---|---|
| Screen | screen-page | .screen-page |
| print-page | .print-page |
The caller still owns the mode and title. The family owns which implementations go together; the export client only knows the two product roles. In this example, print changes typography in an HTML fragment. It does not produce a paginated PDF.
02Let one family supply both
The client requests roles, not brands.
Follow ExportFactory and the export client below. The client asks one selected
factory to create a page renderer and a stylesheet, then assembles their output. It has no
screen-or-print branch. This is the whole program the lab runs: two families, the client, and
two callers that each choose a family once.
interface PageRenderer {
render(title: string): string;
}
interface Stylesheet {
css(): string;
}
// The abstract factory: one contract that creates both product roles.
interface ExportFactory {
createPage(): PageRenderer;
createStyles(): Stylesheet;
}
const makePage = (className: string): PageRenderer => ({
render: (title) => `<article class="${className}"><h1>${title}</h1></article>`,
});
const makeStyles = (className: string, font: string): Stylesheet => ({
css: () => `.${className} { font-family: ${font} }`,
});
// Two concrete families. Each creates a page and a stylesheet that agree.
const screenFactory: ExportFactory = {
createPage: () => makePage('screen-page'),
createStyles: () => makeStyles('screen-page', 'sans-serif'),
};
const printFactory: ExportFactory = {
createPage: () => makePage('print-page'),
createStyles: () => makeStyles('print-page', 'serif'),
};
// The client asks one family for both roles. It never names screen or print.
function exportDocument(factory: ExportFactory, title: string): string {
const page = factory.createPage();
const styles = factory.createStyles();
return `<style>${styles.css()}</style>${page.render(title)}`;
}
// Two callers, each choosing a family once.
const editor = exportDocument(screenFactory, 'Release notes');
const report = exportDocument(printFactory, 'Release notes');
console.log(`editor: ${editor}`);
console.log(`report: ${report}`); | Role | Responsibility | What it handles |
|---|---|---|
| Caller | Supplies a family | Reads the user’s intent, chooses the family once per export job, and decides what to do with the HTML. |
| Factory | Creates the pair | Chooses implementations that agree. The products own rendering and CSS; the factory does not perform the export. |
| Export client | Uses the products | Requests the two collaborators and assembles their output, through their contracts, with no mode branch. |
“Abstract” means that the client works through a contract. Here that is a TypeScript interface, satisfied by plain objects; it needs no abstract base class or inheritance. A third family is one more implementation of the contract, and neither the client nor its callers change.
03Predict the mixed pair
A caller that skips the factory.
Experiment
Two families, one client
// A third caller, the preview, was written before the factory. It picks each
// product itself: its page line learned about print, its stylesheet line did
// not. Add after line 42:
const page = printFactory.createPage();
const styles = screenFactory.createStyles();
const preview = `<style>${styles.css()}</style>${page.render('Release notes')}`;
console.log(`preview: ${preview}`); Reading the TypeScriptInterfaces, object literals and closures
ExportFactory describes two methods. An object with those methods satisfies the
interface without extending a class, which is also why a page from either family fits
wherever a PageRenderer is expected: TypeScript compares shapes, not origins.
createPage: () => makePage('screen-page') creates a product when called. It
does not hold one already-created page. The product’s render closes over its class
name, so a later family selection does not change it.
The production version marks className readonly, which restricts
assignment through the interface at compile time; it does not freeze objects at runtime.
Its failures throw, so the caller catches them.
04Meet it in your language
Families you already choose between.
TypeScript on the server meets this pattern where several values must share one resource. A checkout writes an order and updates the user’s last order, and both must commit or roll back together, so both stores have to run on the same transaction. A handler that builds each store itself can build one on the transaction and the other on the pool: the user update then commits at once, and survives the rollback when the order insert fails.
Prisma’s interactive transactions (4.7 and later) hand you the fix as a family: inside prisma.$transaction(async (tx) => …), tx.user and tx.order share the transaction, and a stray prisma.user does not.
Below, the same idea without a library: Stores creates both stores, and the
transaction’s family builds them on the transaction.
// The product roles: the two stores a checkout writes to.
interface Orders {
add(userId: string, orderId: string): Promise<void>;
}
interface Users {
setLastOrder(userId: string, orderId: string): Promise<void>;
}
// Anything that runs a statement: the pool, or one transaction on it.
interface Queryable {
query(sql: string, params: unknown[]): Promise<unknown>;
}
interface Pool extends Queryable {
transaction<T>(work: (tx: Queryable) => Promise<T>): Promise<T>;
}
const sqlOrders = (db: Queryable): Orders => ({
add: async (userId, orderId) => {
await db.query('INSERT INTO orders (id, user_id) VALUES ($1, $2)', [orderId, userId]);
},
});
const sqlUsers = (db: Queryable): Users => ({
setLastOrder: async (userId, orderId) => {
await db.query('UPDATE users SET last_order = $1 WHERE id = $2', [orderId, userId]);
},
});
// Before: the handler builds each store itself, and one of them on the pool.
export function checkoutBefore(pool: Pool, userId: string, orderId: string) {
return pool.transaction(async (tx) => {
await sqlUsers(pool).setLastOrder(userId, orderId);
await sqlOrders(tx).add(userId, orderId);
});
}
// After: a family of stores. The transaction's family builds both on it;
// a test can pass an in-memory family with the same two methods.
interface Stores {
orders(): Orders;
users(): Users;
}
const txStores = (tx: Queryable): Stores => ({
orders: () => sqlOrders(tx),
users: () => sqlUsers(tx),
});
// checkout asks the family for both stores and never names the database.
async function checkout(stores: Stores, userId: string, orderId: string) {
await stores.users().setLastOrder(userId, orderId);
await stores.orders().add(userId, orderId);
}
// The caller owns the transaction and hands its family over.
export function checkoutInTx(pool: Pool, userId: string, orderId: string) {
return pool.transaction((tx) => checkout(txStores(tx), userId, orderId));
} The caller owns the transaction: it begins it, hands the family to the code that needs both
stores, and the commit or rollback covers both. checkout never names the database,
so a test can pass an in-memory family with the same two methods.
05In the frontend
Components that must come from one kit.
The browser keeps two families of elements. document.createElement makes HTML
elements; document.createElementNS with the SVG namespace makes SVG elements, and
each family’s products only work inside that family. Ask the HTML family for a circle and you get an HTMLUnknownElement: it sits inside your <svg> and draws nothing. Frameworks choose the family for you. React chooses
while rendering: entering <svg> switches to the SVG namespace, so a Link component that returns <a> becomes an SVG link inside a chart.
Your own code chooses a family the day a white-label app gives each customer a brand kit: a field wrapper and a text input, styled to go together. A screen that imports components itself can wrap a plain input in a branded field, the preview’s mix with components as the products. The fix is the lesson’s arrangement: choose the kit once, near the root, and hand it down through context.
import { createContext, useContext, type ComponentType, type ReactNode } from 'react';
type FieldProps = { label: string; children: ReactNode };
type InputProps = { name: string };
// Two brands' components. Each brand's field and input are styled to go together.
const PlainField = ({ label, children }: FieldProps) => (
<label className="plain-field">{label}{children}</label>
);
const PlainInput = ({ name }: InputProps) => <input className="plain-input" name={name} />;
const BrandField = ({ label, children }: FieldProps) => (
<label className="brand-field">{label}{children}</label>
);
const BrandInput = ({ name }: InputProps) => <input className="brand-input" name={name} />;
// Before: the screen imports components itself. Its field moved to the
// customer's brand and its input did not.
export function SettingsFormBefore() {
return (
<form>
<BrandField label="Email">
<PlainInput name="email" />
</BrandField>
</form>
);
}
// After: a kit supplies every role, and the app chooses one kit per customer.
export type Kit = { Field: ComponentType<FieldProps>; TextInput: ComponentType<InputProps> };
export const plainKit: Kit = { Field: PlainField, TextInput: PlainInput };
export const brandKit: Kit = { Field: BrandField, TextInput: BrandInput };
const KitContext = createContext<Kit | null>(null);
// Chosen once, near the root: <KitProvider kit={customer.kit}>.
export function KitProvider({ kit, children }: { kit: Kit; children: ReactNode }) {
return <KitContext value={kit}>{children}</KitContext>;
}
function useKit(): Kit {
const kit = useContext(KitContext);
// Required, so a screen outside the provider fails instead of guessing a kit.
if (!kit) throw new Error('render inside <KitProvider>');
return kit;
}
// The form asks for roles and never names a brand.
export function SettingsForm() {
const { Field, TextInput } = useKit();
return (
<form>
<Field label="Display name">
<TextInput name="name" />
</Field>
<Field label="Email">
<TextInput name="email" />
</Field>
</form>
);
} The form asks for roles and never imports a brand. A new customer is one new kit; a new role,
such as a date field, has to join every kit. Keep the provider required, so a screen rendered
outside it fails instead of falling back to some default kit. <KitContext value> as a provider needs React 19; earlier versions write <KitContext.Provider>.
In Svelte the kit travels the same way, through setContext and getContext, and a component held in a variable renders as a tag, so the form reads <Field> just as in React. Svelte differs on element families: it decides them
per file, at compile time. A link wrapper whose only element is <a> compiles to
an HTML anchor, and inside a chart the circles it wraps do not draw (Svelte 5.57); an SVG-only root
such as <g> makes the file SVG, so a chart link and a page link become two
components.
06Compare the choices
Which change should this design make easy?
Imagine an editor’s Export action. Its command handler snapshots the current title and selected mode, resolves a factory, and calls the export client. A scheduled report can use that same client with a print factory. Each caller owns its choice of family and what to do with the returned HTML.
What a production exporter addsSelection by name, escaping, and a check on the pair
The same families, checked. The caller selects a family by name, and an unknown name is an error before any product is made. The title is escaped as HTML text. Assembly then rejects an empty title, and a page and a stylesheet whose class names differ, in that order. A whitespace-only title is accepted: it is text.
export interface PageRenderer {
readonly className: string;
render(title: string): string;
}
export interface Stylesheet {
readonly className: string;
css(): string;
}
export interface ExportFactory {
createPage(): PageRenderer;
createStyles(): Stylesheet;
}
// HTML text escaping: & first, so the entities added after it stay intact.
const escapeText = (text: string) =>
text
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
const makePage = (className: string): PageRenderer => ({
className,
render: (title) => `<article class="${className}"><h1>${escapeText(title)}</h1></article>`
});
const makeStyles = (className: string, font: string): Stylesheet => ({
className,
css: () => `.${className} { font-family: ${font} }`
});
export const screenFactory: ExportFactory = {
createPage: () => makePage('screen-page'),
createStyles: () => makeStyles('screen-page', 'sans-serif')
};
export const printFactory: ExportFactory = {
createPage: () => makePage('print-page'),
createStyles: () => makeStyles('print-page', 'serif')
};
// The caller names a family once; nothing else in the export names one.
export function selectExportFactory(family: string): ExportFactory {
if (family === 'screen') return screenFactory;
if (family === 'print') return printFactory;
throw new Error(`unknown export family: ${family}`);
}
// The type system does not know which family a product came from, so check.
export function assemble(page: PageRenderer, styles: Stylesheet, title: string): string {
if (title === '') throw new Error('title must not be empty');
if (page.className !== styles.className) {
throw new Error('page and stylesheet must belong to the same family');
}
return `<style>${styles.css()}</style>${page.render(title)}`;
}
export function exportDocument(factory: ExportFactory, title: string): string {
return assemble(factory.createPage(), factory.createStyles(), title);
} Lifetime, concurrency, and partial creationWhen the products get real
The sample’s products are stateless after construction. Each creation method returns a new product value or object; each export returns a new string. A family selected for a second job does not mutate the first job’s products. There is no connection, listener, or handle to clean up. Our products are stateless closures. A product that captures something mutable, say a shared options object, would let one export’s change reach another’s product.
A real renderer might allocate a browser page or load fonts. Then creation can fail halfway through: if the page succeeds but the stylesheet loader fails, somebody must release the page. Put that responsibility in an explicit assembly operation or caller scope and expose asynchronous errors where needed.
A factory interface does not specify caching, thread safety, or disposal. Decide whether products are fresh or shared and who closes them. For concurrent jobs, pass job inputs explicitly; a global “current family” that changes between the two creation calls can undermine the pairing.
Use the pattern when several product kinds vary together and clients need a shared way to create them. Name the compatibility rule and the caller that needs creation before adding the contract.
| The pressure | What Abstract Factory buys or costs |
|---|---|
| A compact export family | Add its products and factory, then expose it at selection points. Keep the generic export client. |
| A table-of-contents product in every export | Extend the factory interface and all concrete families. Update the consuming client. |
| Only text color and spacing change | A theme value or CSS custom properties may express the whole difference. Separate product factories may add little. |
| A caller receives one fixed pair once | Pass a finished bundle, possibly built by one ordinary function. There may be no reason to expose creation methods. |
| Products can mix freely | Independent collaborators or strategies preserve those combinations more clearly than a forced family. |
For this one-heading exporter, I’d keep a simple function or finished bundle. The factory contract earns its cost when product roles have real family-specific behavior and several clients need the same creation boundary.
Watch for a factory accumulating unrelated services just because they are used on the same screen. A logger, a clock, and a page renderer do not automatically form a family. Name the compatibility rule or shared creation policy that justifies selecting them together. And some apparent families vary independently: if every renderer should work with every stylesheet, a forced screen-or-print pairing restricts useful combinations. Put in the family what must vary together, and pass what varies on its own as a value or a separate collaborator.
Its neighbours: a factory function gives one creation decision a name, and can return a finished bundle instead of a family. Builder exposes steps for assembling one result; Strategy supplies interchangeable behavior for one operation, and a family may create one. Bridge lets two axes, such as a chart and its drawing surface, vary independently, where a family makes them arrive matched. Dependency injection describes who supplies the factory; passing it to the export client is injection.
07Make the call
Explain what must agree.
Before you open the answer below, say it without the pattern name: what must agree, who chooses the family and when, and what a third family and a third product role each cost.
One way to say itWhat agrees, who chooses, and what a change costs
“The export mode chooses one provider. The exporter asks that provider for a renderer and a stylesheet, so their class names stay together.” The caller chooses the family once per job; the client asks it for every product and never names a family, so a third family is one more implementation and no client changes. A third product role is the expensive direction: the contract, every family and the client that uses it all change. The pattern keeps a pair together only for callers that go through the family; a caller that picks products itself can still mix them, which is why a production exporter checks the pair, or a Rust one makes the family part of the type. And when the products differ only in data, or can mix freely, a value or independent collaborators are simpler than a family.
Practice in the lab
A travel booking app sends each customer a confirmation and a payment reminder, and both show a price and a travel date in the customer’s locale. Like the lesson’s page and stylesheet, the price format and the date format must come from one family. The team keeps one setting, the default locale, for customers whose own locale has no formats yet. Each level starts from the previous level’s finished code, so you can begin at any of them.
- ABSTRACT-FACTORY-101 (easy) German reminders show an English date
- ABSTRACT-FACTORY-102 (medium) French customers get English formats Pro
- ABSTRACT-FACTORY-103 (hard) Review: a German customer paying in dollars is billed in euros Pro