← Design patterns
Creation Factory functions

Factory

Let the caller say what it needs.

You’ve probably written one already. A makeUser() in a test, a function that sets up a client, a helper that sorts out the defaults. Let’s look at what those functions have in common, and when that extra name actually helps.

TypeScriptGoPythonSame contract · across the comparison.

01 / The idea

At some point, “just make an object” gets a little less simple.

You’re setting up a build. Library builds need one output format, app builds need another. Production usually means minified output. Sometimes you deliberately turn that off because you’re trying to understand a problem.

None of that is especially complicated. The awkward part arrives when three different callers each have their own version of those decisions. A default changes. Two callers get updated. The third still works, just differently.

A factory is a function whose job is to create something. It gives those creation decisions a home, so a caller can describe what it needs without assembling every detail itself.

If you write components, you call one on every line of markup. <section> and <Profile /> are both requests to create something, and the first letter tells the compiler which: an HTML element or your component. That is why component names start with a capital letter.

That’s the part I want you to notice: who has to know the rules? If the answer is “everyone who needs one of these,” a factory might make the code easier to live with.

Why “Factory” can mean a few different thingsNames in the family

This lesson starts with factory functions: a broad, everyday creation technique. Factory Method is a more specific pattern where a creation method can be overridden by subclasses. Abstract Factory groups the creation of related products that need to fit together.

You don’t need to memorize the family tree before using a creation function. It is useful to know which conversation a name belongs to.

02 / See the shape

Start small. Then give it a job.

The basic form puts a target default in one place and accepts a minification choice. It’s small on purpose. Follow the return value, then switch to In the wild to see what happens when creation has more decisions to make.

Every language in the comparison solves the same problem. We keep the contract lined up, while letting each language say it in its own way.

One function, one small default. This is already a factory. Whether it deserves a name in your codebase is the more useful question.

TypeScriptReading
config.ts
export function createBasicConfig(minify = false) {
	return { target: 'es2022', minify };
}

const basic = createBasicConfig(false);
GoAlongside
config.go
type BasicConfig struct {
	Target string
	Minify bool
}

func CreateBasicConfig(minify bool) BasicConfig {
	return BasicConfig{Target: "es2022", Minify: minify}
}

var basic = CreateBasicConfig(false)
Reading the TypeScript??, optional fields, Readonly

minify?: boolean lets the caller leave the choice out. ?? uses a fallback only for null or undefined, so false survives. That one distinction carries a lot of this example.

Readonly describes the result to the type checker. Object.freeze also prevents changes to its own properties at runtime.

The preset and mode arrive as strings. We check them before returning a narrower, known set of values. This still assumes callers satisfy the other declared input types. An arbitrary JSON payload needs validation at its boundary.

Reading the Go*bool, nil, & and error

A plain bool starts at false. On its own it cannot tell us whether the caller chose false or supplied nothing. A *bool gives us three cases: nil, a pointer to false, or a pointer to true.

&minify takes the address of the caller’s value. *overrides.Minify reads that value after we check the pointer is not nil. The returned configuration holds the boolean itself, so it does not keep that pointer.

(BuildConfig, error) makes failure part of the function’s signature. A caller checks the error before using the result. We use an options struct here; functional options are another API design to compare once there is a reason for them. On failure, the zero-valued struct returned beside the error is a placeholder, not a valid configuration.

Reading the PythonDataclasses, optional values, and a small validator

Python represents the optional override with None. The expression chooses the default only when the field is None, so an explicit False survives just as it does in TypeScript and Go.

Frozen dataclasses give the returned configuration value-style fields and reject later assignment. This is shallow immutability: it is useful here because every field is a scalar, not a promise that nested objects could never change.

What counts as an empty directory?One shared validation rule

An empty string, whitespace-only input, or a byte-order mark on its own is rejected. Every implementation uses the same Unicode whitespace rule, with the byte-order mark included explicitly. A nonblank directory keeps its exact text; the factory does not normalize paths or check the filesystem.

03 / Follow the values

Before you change it, make a guess.

The build below starts in production mode. What should happen if you explicitly turn minification off? Try it. Then remove the override and watch the default take over again.

Watch where the returned value comes from, or step through at your own pace. In Try it, your inputs run the same function you just read. The diagram and table follow the returned fields back to the decisions that supplied them.

Factory

A default is an offer.

CALLER REQUESTlibrarymode: production
MODE RULEtrue production mode
CALLER OVERRIDENot supplied Use the mode rule
RETURNED CONFIGURATION

minify

true

The mode supplies this value.

es2022esm

Preset library; mode production; minify true, from the mode rule. Target es2022; format esm; output directory dist/library.

01/ 04
Resolve production

Let the mode supply it.

Production supplies true when the caller leaves minification out.

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

Read this scene

Production supplies true when the caller leaves minification out.

Preset library; mode production; minify true, from the mode rule. Target es2022; format esm; output directory dist/library.

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

04 / Try a decision

There’s a tiny bug hiding in a very reasonable-looking line.

Someone simplified the minification rule. It still passes a test where no override is supplied. The interesting test is the one where the caller disagrees with the default.

Production build. Explicit false. Which TypeScript expression respects the caller?

Here, production is true. We want a missing override to use it, and an explicit false to stay false.

05 / Give it a real job

One build runner. Several ways to arrive.

Imagine a repository with a CLI command, a watch task, and a release task. All three need a configuration for the same build engine. The CLI reads flags, the watch task chooses development mode, and the release task asks for a production library build.

Each caller turns its inputs into options and calls resolveBuildConfig. The runner receives the finished value. If the default output directory changes, we change it where creation happens. If a release task needs readable output while investigating a bug, it can still say so explicitly.

Our example stops at the configuration; a real build engine would receive that result and do the work. Its presets would reflect your project’s needs, so esm, iife, and es2022 are this example’s choices rather than Vite’s API.

Caller

Knows the situation

CLI flags, watch mode, release intent.

Factory

Knows how to create it

Defaults, presets, overrides, validation.

Build runner

Uses the result

Receives a configuration and runs the build.

Notice that the runner does not need a factory parameter just because factories are easy to replace in tests. If it only needs one finished configuration, pass that value. Pass a factory when the runner needs to decide when or how often to create another one. That decision is our first bridge to dependency injection.

Build UIs?Every component tag you write is a creation request, and one day you will write the factory yourself.

Where it already is in your components

JSX and Svelte markup turn every tag into a request to create something, and the compiler decides what from one detail: the tag’s first letter. <section> asks for an HTML element. <Profile /> asks for the component called Profile. Both React and Svelte document this as the capitalization rule. Underneath it is a factory choosing its product.

The rule bites when a variable holds the component. In React, const component = kinds[type] followed by <component /> compiles to jsx("component", …), a string, while <Component /> compiles to jsx(Component, …), the function itself. Given the string, React asks the browser’s own factory, document.createElement, for a <component> element, gets an unknown element back, and warns in development that the tag “is unrecognized in this browser.” Your component never runs.

Svelte makes the same decision while compiling, and the difference is in what it catches. Import a component under a lowercase name and the compiler warns that the tag “will be treated as an HTML element unless it begins with a capital letter.” Keep it in a lowercase variable instead and the compiler says nothing: the page renders an empty <component> element. The earliest clue in either framework is your linter reporting component as unused, because the tag never reads the variable. In a React .tsx file, TypeScript also rejects the tag: “Property 'component' does not exist on type 'JSX.IntrinsicElements'.” In both frameworks a hyphenated tag such as <profile-card> takes a third path and becomes a custom element.

When you have to own it

Now the page comes from a CMS. Each block arrives as data: a type such as "hero" and whatever props the editor filled in. Something has to turn that string into a component, fill in the defaults the editor left blank, and decide what an unknown type does. Written inline, that switch lands in the page, the preview pane, and the email renderer, each a little different: the drift from section 01, with components in place of build options.

Give it one home. The factory below maps each type to a component and its defaults. It uses ??, so an editor’s explicit false for “show author” survives the default, the same decision as section 04. It returns nothing for a type this build doesn’t know, so the page can show that block as unsupported instead of guessing. And the component it hands back still goes into a capitalized name before it reaches a tag.

blocks.ts
// A CMS sends each page block as data: a type string and the props an editor filled in.
export type BlockData = { id: string; type: string; props: Record<string, unknown> };
export type BlockKind<C> = { component: C; defaults: Record<string, unknown> };
export type MadeBlock<C> = { Component: C; props: Record<string, unknown> };

// One home for what a block becomes: which component, which defaults, and what
// an unknown type does. Framework-free, so React and Svelte pages share it.
export function createBlockFactory<C>(kinds: Record<string, BlockKind<C>>) {
	return function createBlock(block: BlockData): MadeBlock<C> | null {
		// Own keys only, so a type called "toString" is unknown rather than inherited.
		if (!Object.hasOwn(kinds, block.type)) return null;
		const { component, defaults } = kinds[block.type];
		const props: Record<string, unknown> = { ...defaults };
		for (const [key, value] of Object.entries(block.props)) {
			// null means the editor left it unset. false is a choice, and ?? keeps it.
			props[key] = value ?? defaults[key];
		}
		// Capitalized, because this value is headed for a tag.
		return { Component: component, props };
	};
}

The same component held under a lowercase and a capitalized name. Only the capitalized tag creates it: React warns at runtime, the Svelte compiler stays silent for a variable, and a linter flags the unused name in both.

ReactAlready in your code
BlockView.tsx
import { Hero, Quote } from './components/react';

const kinds = { hero: Hero, quote: Quote };

export function BlockView({ type, title }: { type: 'hero' | 'quote'; title: string }) {
	// The same component, held under two names. JSX decides what a tag
	// creates from its first letter and never looks at what the variable holds.
	// Your linter already sees the mistake: it reports component as unused,
	// because the lowercase tag below never reads it. Silenced only to show it.
	// eslint-disable-next-line @typescript-eslint/no-unused-vars
	const component = kinds[type];
	const Component = kinds[type];
	return (
		<>
			{/* Compiles to jsx("component", …): a string. React asks the browser for a
			    <component> element, Hero never runs, and development builds warn:
			    "The tag <component> is unrecognized in this browser. If you meant to
			    render a React component, start its name with an uppercase letter." */}
			{/* TypeScript stops it sooner, reporting "Property 'component' does not
			    exist on type 'JSX.IntrinsicElements'."
			    @ts-expect-error this sample keeps that error on purpose */}
			<component title={title} />
			{/* Compiles to jsx(Component, …): the function itself, so Hero renders. */}
			<Component title={title} />
		</>
	);
}

06 / Recognize it elsewhere

The same idea wears a few different outfits.

Configuration is one useful example. These are other creation situations you’ll recognize. Treat them as ways to look at the idea, rather than four more names to memorize.

Factory situations and what each owns
What you’re creatingA familiar callWhat creation owns
A valid valueparseAmount(input)Acceptable input and the guarantees of the returned value.
A configured clientcreateApiClient(options)Setup, defaults, and the collaborators it needs to work.
Independent statecreateStore(initial)A fresh instance and the operations that act on its state.
An implementationcreateStorage(kind)Choosing a concrete implementation behind a shared contract.

A fixture helper belongs here too. makeUser({ email }) can give a test a valid starting point. It earns its place when it makes the relevant difference easy to see; it gets in the way when its hidden defaults are the reason a test passes.

07 / Already in your toolbox

You may have been using the idea before you had a name for it.

Here are three concrete APIs to look at. For each one, follow what the function creates and which details it takes care of.

React · createElement

Creates a React element object from a type, props, and children. The returned element is a description React can render; creation has its own rules, including special treatment of keys.

Open the API explanation ↗

Svelte · writable

Creates a store with a small interface for reading and changing its value. The caller gets an object it can use without assembling its subscription machinery.

Look at the store contract ↗

Go · http.NewRequest

Creates a request from a method, URL, and body. The API handles setup details and returns an error when creation fails. A request value is what the next part of the HTTP client needs.

Follow the standard-library API ↗
A useful counterexample: Vite’s defineConfigA name can mislead

It looks like our configuration factory, but Vite v6’s defineConfig simply returns its input. It exists as a type helper; the resolution work lives elsewhere. That is a useful reminder to inspect the behavior before assigning a pattern to a familiar-looking name.

Read the pinned Vite v6.0.0 source. Our example is called resolveBuildConfig to keep that distinction visible.

08 / The parts to watch

Creation is only one moment in a value’s life.

A factory can gather important decisions. It cannot make every decision that follows disappear. These are the edges worth keeping in view.

Valid when created does not always mean valid forever

If callers can later mutate a value, they may break the rule the factory established. Decide whether that matters for this type. A factory does not automatically promise immutability.

Our TypeScript result is frozen, and every field is a primitive. Add an array of plugins and freezing only the outer object would leave that array mutable. Our Go result is a mutable copy with scalar fields. Add a map, slice, or pointer and some underlying data may be shared. Python's frozen dataclass rejects field assignment. Each visible implementation still follows its language’s ordinary mutability rules.

JavaScript’s shallow freeze and Go’s value representation explain the limits.

The factory starts collecting everyone’s special case

One new option can be useful. A growing collection of unrelated flags is a reason to pause. Are these callers still asking for the same thing? Sometimes a named preset helps; sometimes the responsibilities have separated enough to deserve different creation functions.

Judge the callers together. Counting options alone won’t tell you whether the API is clear.

Creation quietly starts doing I/O

Fetching credentials, opening a socket, or loading a file changes what a caller must account for: latency, failure, cancellation, and cleanup. Make that work visible in the name and contract. In TypeScript, asynchronous creation returns a promise; in Go, callers may need a context and an error path.

For a client used by a frontend, ask which values belong to the instance and which must be read per request. A token captured once at creation may be stale later. The factory’s lifetime is part of the design.

A factory was added only to make a test easier

Being able to substitute creation is useful when creation is a real dependency. If a function only uses a finished value, passing that value can be simpler. If it needs a collaborator, a small interface or function parameter may be enough.

The interesting test here is a behavior claim: an explicit false survives a production default, and every implementation runs that same case.

09 / Make the call

What would you have to change tomorrow?

This is how I’d decide whether to keep the factory. Give the code a plausible change and follow the work it creates.

How a change affects inline construction and a shared factory
The changeRepeated inline constructionA shared factory
The shared output-directory default changesFind and update each copy of that default.Update the rule once. Existing overrides still win.
An empty directory must be rejectedEach creation path needs the check, or another shared boundary.Add the check at the creation boundary.
One caller needs an unrelated configurationThat caller can describe its own value directly.Adding a special option may complicate everyone’s API.

Reach for it when creation has a responsibility worth naming. Shared defaults that must stay consistent, a meaningful invariant, choosing an implementation, or assembling a working object can each be a reason.

Keep the literal when the caller already has the complete, obvious value. A single clear construction site does not need a helper just to earn a pattern name. And keep a constructor when it already provides a clear API for the job.

The question I’d leave beside the code is: what does the caller get to stop knowing because this function exists? If you can answer that plainly, you can usually explain the design plainly too.

10 / Take the idea with you

Try explaining the decision without saying “factory.”

“I put the build defaults here so the release and watch tasks agree. Callers can still override the parts they own.” That says more about the design than the pattern name alone.

Before moving on, jot down what creation owns in this example, why false needed special care, and one place in your own code where a creation function would be useful, or unnecessary.

Connections to follow nextRelated lessons

Practice the pattern

Three factory tasks from everyday code.

Choose from three common creation tasks: centralize build defaults, select a notification sender, or choose a storage adapter. Open the workspace and switch exercises from its toolbar; each one keeps its own TypeScript and Go draft, checks, hints, and worked solution.

Factory practice 8 min

Complete resolveBuildConfig

This is an experiment with ticket-style exercises, giving beginners a feel for how tasks may be described in the workplace. Leave feedback

This practice workspace is open to everyone. A free account syncs your lesson progress across devices.

TypeScript Go

Factory practice 8 min

This is an experiment with ticket-style exercises, giving beginners a feel for how tasks may be described in the workplace. Leave feedback

Complete resolveBuildConfig

TYPESCRIPT

Work item BUILD-142

Complete resolveBuildConfig

Implementation exercise Ready

Context

The CLI, watch task, and release task have started to disagree about build defaults. Callers need one consistent configuration, but must still be able to override the settings they own.

Impact

Different defaults can send the same build to different formats or directories. Release engineers also need to turn minification off when they are investigating an output.

Example

A production library build with minify: false should keep minification off, use ESM, and default to dist/library.

Acceptance criteria
  1. AC-1App builds use IIFE format and default to dist/app; library builds use ESM and default to dist/library.
  2. AC-2Mode defaults to development; production enables minification and development leaves it off.
  3. AC-3An explicit minify: false is preserved in production, and a caller-provided output directory is respected.
  4. AC-4Unknown presets, unknown modes, and blank output directories are rejected with useful errors.
Notes
  • Target is es2022 for both presets.
  • Treat false as an intentional override; absence and false are different states.
  • TypeScript returns a configuration or throws. The Go version returns (BuildConfig, error).

Copy the ticket to research the problem in your own notes or AI tool. Your code stays here.

Your implementation

Syntax errors are marked as you type. Run the visible checks to see whether the behavior meets the ticket; your code stays in this tab.

Checks cover

  • App preset uses IIFE and dist/app
  • Library preset uses ESM and ES2022
  • Production defaults to minified
  • Development defaults to readable output
  • Explicit false survives production mode
  • Caller output directory is respected
  • Unknown presets are rejected
  • Unknown modes are rejected
  • Blank output directories are rejected

Take an example into your editor. Change a rule, add a case, and see whether the caller needs to know about it.

Back to design patterns →