← Design patterns
Creation Families of products

Abstract factory

Create things that belong together.

Your editor can export a page for the screen. Now it needs a print version too. You change the HTML renderer, but the stylesheet still expects the old class names. Each piece works on its own; the pair no longer agrees.

Let’s give that agreement a home. We’ll choose an export family once, then ask it for the renderer and stylesheet that belong together.

TypeScriptGoPython Same contract · across the comparison.

01 / What needs to agree

The useful unit is the family.

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.

The pressure appears when the editor, a report job, and a preview panel all make those choices. A new print renderer is added in each place. One stylesheet choice is missed. The renderer emits print-page; the CSS targets .screen-page. The browser still displays a heading, so the mistake may look like a styling bug.

Abstract Factory provides a common interface for creating several kinds of related products, with a concrete factory for each family. In our example, the product kinds are renderer and stylesheet. Screen and print are the families. The export client requests both products through the factory interface.

Read across for a compatible family; down for alternatives to each product role.
Concrete familyCreates a page rendererCreates a stylesheet
ScreenEmits screen-pageTargets .screen-page
PrintEmits print-pageTargets .print-page

The caller still chooses screen or print. What moves is the knowledge of which implementations go together. The export client can now say, “give me your page renderer and your stylesheet,” without choosing either concrete implementation.

Here, abstract means the client works through a contract. TypeScript can express it with an interface and an object containing functions. You do not need an abstract base class, inheritance, or a factory registry to get the relationship.

02 / See the shape

Two product roles. One creation contract.

Start with ExportFactory and follow exportDocument. It asks the supplied factory to create both collaborators, then assembles their output. There is no screen-or-print branch inside that client.

In the useful application, two factories supply matching class names and CSS selectors. The renderer escapes the title as text; the stylesheet owns its rules. Small helper implementations are parameterized by the family instead of repeating four nearly identical classes.

The result is a styled HTML fragment containing one heading. “Print” supplies serif, black text and more padding rather than a paginated PDF.

Two product interfaces, one factory interface, and a client that asks it for both products. The assembly helper and concrete families appear in the next view.

TypeScriptReading
export.ts
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;
}

// This client knows product roles, but no screen or print implementations.
export function exportDocument(factory: ExportFactory, title: string): string {
	const page = factory.createPage();
	const styles = factory.createStyles();
	return assemble(page, styles, title);
}
GoAlongside
export.go
type PageRenderer interface {
	ClassName() string
	Render(title string) string
}

type Stylesheet interface {
	ClassName() string
	CSS() string
}

type ExportFactory interface {
	CreatePage() PageRenderer
	CreateStyles() Stylesheet
}

// This client knows product roles, but no screen or print implementations.
func ExportDocument(factory ExportFactory, title string) (string, error) {
	page := factory.CreatePage()
	styles := factory.CreateStyles()
	return Assemble(page, styles, title)
}

Every implementation returns the same HTML for the same title and family. They reject an unknown family, the exact empty title, or a mismatched pair. Whitespace-only titles remain text.

Reading the TypeScriptInterfaces and closures

ExportFactory describes two methods. An object with those methods satisfies the interface without extending a class. screenFactory() is itself an ordinary factory function: it returns the object that implements our Abstract Factory contract.

createPage: () => makePage('screen-page') creates a product when called. It does not hold one already-created page. The product’s render function closes over its class name, so a later family selection does not change it.

readonly restricts assignment through this TypeScript interface; it does not freeze objects at runtime. Failures throw, so the caller catches them.

Reading the GoImplicit interfaces and errors

ScreenFactory is an empty struct with methods. Those methods satisfy ExportFactory implicitly: there is no implements declaration. Returning PageRenderer hides the concrete htmlPage behind the operations the client needs.

The receiver in func (p htmlPage) Render(...) is the page value on which the method acts. These structs contain strings and use value receivers; there is no shared mutable map or slice in the sample.

(string, error) makes the failure path visible at the call site. Check the error before using the string. The sample assumes valid, non-nil collaborators supplied by its factories; the interface does not prevent a custom factory from returning nil.

Reading the PythonProtocols and concrete products

Protocol describes the product and factory shapes for type checkers. Python does not need a base class or an explicit implements declaration; an object with the required attributes and methods can satisfy the contract statically.

The concrete factories return frozen dataclass products. That keeps this sample's products value-like and independent after creation; it is not a runtime guarantee for arbitrary objects accepted through the protocol.

Selection and assembly raise ValueError on failure. The caller must catch it before using an HTML result, just as the other languages handle their error paths.

03 / Follow both products

Change the family. Then break the agreement.

Watch both products change, or step through the choices yourself. In Try it, predict the two class names before selecting print. Then choose the stylesheet separately and leave it on screen. Does a shared product interface alone make that combination valid?

Abstract factory

One family. Two products.

ONE FAMILYscreen kit
PAGE RENDERER
<article class=screen-page>…</article>

Emits the page class.

STYLESHEET
Targets this selector:.screen-page{ font-family: … }

Styles the matching class.

Both products agree.

The client can assemble screen-page with .screen-page.

Page class: screen-page. Stylesheet selector: .screen-page. A matching HTML fragment was returned.

01/ 04
Create screen products

One family, two products.

The page class and the stylesheet selector agree.

Reduced motion: choose a scene to see its completed state.

Read this scene

The page class and the stylesheet selector agree.

Page class: screen-page. Stylesheet selector: .screen-page. A matching HTML fragment was returned.

Watch restarts when you return. Step through keeps your selected step. Try it starts with fresh inputs each time you open it.

The ordinary path asks one factory for both products. The separate-selection path demonstrates a caller bypassing that boundary. The compatibility check rejects the mixed pair before returning HTML; without it, the stylesheet’s selector would miss the article.

A class-name comparison is enough for this tiny example, but a custom factory could still return incorrect CSS while reporting the expected name. Stronger systems use private constructors, family-specific types, or runtime identity checks: the pattern arranges the responsibility, and the implementation enforces the promises you need.

04 / Try a decision

Where should the next mode be added?

The first screen-only exporter was reasonable. Now two callers need both modes. Choose a change and follow the work it leaves for the next person.

Print arrives in the editor and the scheduled report job. Where should the pairing decision live?

Both callers must produce HTML whose page class has matching CSS. We want adding another export family to leave those callers’ product choices together.

05 / Give it a real job

The export command chooses. The client assembles.

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.

01 / Caller

Supplies a family

Reads user intent, resolves the mode once per export job, handles errors, and sends successful output to a preview or file service.

02 / Factory

Creates the pair

Chooses implementations that agree. The product methods own rendering and CSS; the factory does not perform the export.

03 / Export client

Uses the products

Requests the two collaborators and assembles their output. It depends on their contracts, so the family can change without a mode branch here.

Suppose a compact export is added. Implement a compact family and register it in the selection function. The generic export client stays the same. The mode picker and any caller that explicitly lists supported modes still need an update.

Now suppose every export must also produce a table of contents. Adding createContents() changes the factory contract, both existing families, and the client that uses the new product. That difference is the central tradeoff: this arrangement welcomes new families more readily than new product kinds.

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.

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.

What this teaching exporter leaves out

Titles are escaped for HTML text, with ampersands escaped before angle brackets and quotes; that protects a text node and does not sanitize rich text. The built-in class names and CSS are trusted constants.

A production exporter needs complete document structure, accessibility metadata, richer content, layout rules, and a delivery mechanism. A print implementation may need page breaks and font-loading guarantees. Add those because the export contract requires them, and keep separate what the caller supplies, what the factory creates, and what its products do.

Build UIs?The DOM already keeps two families of elements, and one day you will choose a family yourself.

Where it already is in your components

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 factory for a circle and you get an HTMLUnknownElement: it sits inside your <svg> and draws nothing. That is the mismatched pair from section 01, built into the DOM, and it looks just as much like a styling bug.

Frameworks choose the family for you, which is why <circle> in a component just works. React chooses while rendering: entering <svg> switches to the SVG namespace, and <foreignObject> switches back. So a Link component that returns <a> becomes an SVG link inside a chart and an HTML link everywhere else.

Svelte chooses while compiling each file, and tags that exist in both families are where that shows. A link wrapper whose only element is <a>, with its content passed in as children, compiles to an HTML anchor. Put it inside a chart and the circles it wraps disappear, with no warning. In Svelte 5.57 the namespace option did not change that for <a>; an SVG-only root such as <g> did. That makes the wrapper a member of the SVG family, so a chart link and a page link become two components.

When you have to own it

Now your own code chooses the family. A white-label app gives each customer a brand kit: a field wrapper and a text input, styled to go together. Screens should ask for roles, “a field” and “an input,” without naming a brand, and one brand’s field should never wrap another brand’s input.

That is this lesson’s arrangement with components as the products. Choose the kit once, near the root, and hand it down through context. The form renders Field and TextInput from the kit 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, the same tradeoff as adding a table of contents to every export family. Keep the provider required, so a screen rendered outside it fails instead of falling back to some default kit.

A link wrapper reused inside a chart. React creates an SVG link there because it reads the parent while rendering; Svelte decides per file, so its chart link needs an SVG-only root.

ReactAlready in your code
Link.tsx
import type { ReactNode } from 'react';

// A link wrapper written for ordinary pages, then reused inside a chart.
function Link({ href, children }: { href: string; children: ReactNode }) {
	// React picks each element's family while rendering, from the parent host
	// element: entering <svg> switches to the SVG namespace and <foreignObject>
	// switches back. Inside the chart below, this <a> is created with
	// createElementNS, so it is an SVGAElement and the circle it wraps draws.
	// On a normal page the same component makes an HTML link.
	return <a href={href}>{children}</a>;
}

type Point = { id: string; href: string; x: number; y: number };

export function Chart({ points }: { points: Point[] }) {
	return (
		<svg viewBox="0 0 200 100" role="img" aria-label="Signups by week">
			{points.map((point) => (
				<Link key={point.id} href={point.href}>
					<circle cx={point.x} cy={point.y} r={8} />
				</Link>
			))}
		</svg>
	);
}

06 / Recognize the relationship

Look for several product roles behind one provider.

The useful connection is who creates the collaborators and why they belong together.

ADO.NET: a concrete family of provider products

System.Data.Common.DbProviderFactory exposes methods such as CreateConnection(), CreateCommand(), and CreateParameter(). They create a provider’s implementations of those different product roles. This closely matches the family-creation contract: code uses common database abstractions while the provider supplies concrete products.

Capability properties indicate support for some products; choosing a provider does not make database dialects interchangeable.

Read the .NET 10 API contract ↗

WebGPU: recognize the creation boundary in a browser API

A GPUDevice creates several resource kinds, including buffers, textures, and render pipelines. One object supplies different collaborators used in rendering, which makes it a useful family-creation analogy for frontend work.

The analogy stops at compatibility: a device also manages and validates resources, and two resources it creates are not automatically usable together.

Read the GPUDevice API ↗

React: a creation function is not enough to establish this pattern

createElement(type, props, ...children) returns a React element. Passing different component types still creates that same kind of description. This API alone does not supply an interchangeable factory contract with separate methods for a family of product roles.

It is a good connection to ordinary factory functions. To identify Abstract Factory, ask where the related product roles and their family-wide selection are.

Read the createElement API ↗
Other places the same arrangement can help

A desktop toolkit can supply buttons and menus for a platform. A storage backend might supply readers and writers using the same encoding. A test environment could create related fake collaborators over one isolated store. In each case, spell out the agreement; “they are related objects” is too vague to justify the extra contract.

Some apparent families vary independently. If you genuinely want every renderer to work with every stylesheet, a forced screen-or-print pairing would restrict useful combinations. Make the two contracts independently composable instead.

07 / Make the call

Which direction does your design need to grow?

Reach for this pattern when several product roles need to vary together, clients need to create them, and a shared interface lets those clients work with different families. Give the code a plausible next change before committing to that interface.

What changes tomorrow?
The pressureWhat Abstract Factory buys or costs
A compact export familyAdd its products and factory, then expose it at selection points. Keep the generic export client.
A table-of-contents product in every exportExtend the factory interface and all concrete families. Update the consuming client.
Only text color and spacing changeA theme value or CSS custom properties may express the whole difference. Separate product factories may add little.
A caller receives one fixed pair oncePass a finished bundle, possibly built by one ordinary function. There may be no reason to expose creation methods.
Products can mix freelyIndependent collaborators or strategies preserve those combinations more clearly than a forced family.

For a real one-heading exporter with a stylesheet difference this small, I’d keep a simple function or a bundle. The factory contract becomes useful when the product roles have substantial, 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.

Factory, Factory Method, Strategy, Builder, and injection
  • Factory functions give creation decisions a name. They can return one value, a finished bundle, or an object that implements an Abstract Factory.
  • Factory Method puts a creation hook in a superclass workflow that subclasses can override. An Abstract Factory can use such methods, but it is the family of product roles that defines this lesson’s scope.
  • Strategy supplies interchangeable behavior for an operation. A factory might create a strategy; selecting one algorithm does not by itself establish a family of products.
  • Builder exposes steps for assembling a result. Abstract Factory exposes creation methods for different related product kinds.
  • Dependency injection describes who supplies collaborators. Passing our factory into the export client is injection; the factory’s product-creation contract answers a different question.

08 / Take the idea with you

Explain what must agree.

Try it without the pattern name: “The export mode chooses one provider. The exporter asks that provider for a renderer and stylesheet, so their class names stay together.”

Now pick a feature you know. Name two product roles, the rule that makes a pair compatible, and who needs to create them. If you cannot name the rule or the need for creation, a smaller design may already fit.

Before moving on, predict the edits for two changes: a third family and a third product kind. Then explain one thing this pattern does not guarantee, such as resource cleanup or compile-time compatibility. Those answers tell you more than remembering a diagram.

Connections to follow nextRelated lessons
  • Factory explores creation rules, defaults, and caller overrides.
  • Dependency injection explains who supplies the contract and who depends on it.
  • Bridge lets a chart and its drawing surface vary as two independent axes, where here the renderer and stylesheet must arrive as a matched family. Registry looks a contributed implementation up by name rather than creating one. Adapter makes an existing interface fit a caller’s contract.

Take the complete files into your editor. Add a compact family, add a shared case, and leave the export client unchanged.

Back to design patterns →