Skip to content

Abstract Factory

When several objects must be chosen together or not at all — creating consistent families, and the mixing bug the pattern exists to prevent.

Intermediate17 minUpdated 8 August 2026

Abstract Factory is the pattern people can recite and cannot motivate. The catalogue example — a window toolkit that renders differently per operating system — has not been a real problem for most engineers in twenty years, so the pattern reads as ceremony.

The motivation that survives is this: sometimes several objects must be chosen together, because a mismatched combination is a bug the type system will not catch.

The bug it prevents

A document service needs a blob store, a queue and a metadata database. In production these are cloud services; in local development they are a directory, an in-memory list and an embedded database.

// Three independent decisions, each read from configuration
const blobs = config.blobDriver === 'cloud' ? new CloudBlobs() : new LocalBlobs();
const queue = config.queueDriver === 'cloud' ? new CloudQueue() : new MemoryQueue();
const meta  = config.metaDriver  === 'cloud' ? new CloudMeta()  : new SqliteMeta();

Nothing here is wrong, and everything here is fragile. There are eight combinations and only two are valid. A developer who sets two of the three variables gets a service that writes blobs to a real bucket and records their location in a database that is wiped on restart — a data-loss bug that passes every test, because every component works perfectly on its own.

Abstract Factory removes the invalid combinations from existence.

classDiagram
    class StorageBackend {
        <<interface>>
        +blobs() BlobStore
        +queue() Queue
        +metadata() MetadataStore
    }
    class CloudBackend {
        +blobs() BlobStore
        +queue() Queue
        +metadata() MetadataStore
    }
    class LocalBackend {
        +blobs() BlobStore
        +queue() Queue
        +metadata() MetadataStore
    }
    class DocumentService {
        -backend: StorageBackend
    }
    StorageBackend <|.. CloudBackend
    StorageBackend <|.. LocalBackend
    DocumentService --> StorageBackend
const backend = config.env === 'production' ? new CloudBackend() : new LocalBackend();

const service = new DocumentService(
  backend.blobs(),
  backend.queue(),
  backend.metadata(),
);

Eight combinations became two. The invalid six are not guarded against, validated or documented — they are unrepresentable, which is a much stronger property than any amount of checking.

The distinction from Factory Method

They are often taught together and the difference is easy to state.

Factory MethodAbstract Factory
Producesone producta family of related products
Varies bywhich subclass you instantiatedwhich factory object you hold
Structureinheritance — a subclass overrides one methodcomposition — you hold a factory and call several methods
Exists tolet a base class avoid naming a productkeep several choices consistent with one another

The one-sentence test: if choosing wrongly for one product and rightly for another would be a bug, you want Abstract Factory. If the products are independent, you do not.

Where it genuinely appears

Database dialects. A connection, a transaction, a query builder and a type mapper must all come from the same dialect. Postgres transactions with MySQL type mapping is not a configuration, it is a defect.

Test doubles as a set. A FakeBackend returning fakes for all three collaborators makes "run this test entirely offline" one decision instead of three, and stops a test from accidentally reaching the network because one of the three was missed.

Cross-platform or multi-tenant rendering. Where a theme, a locale or a tenant must apply consistently across every component produced.

Protocol versions. A request encoder, a response decoder and an error mapper must all speak the same version of a wire protocol.

The shared shape: partial application of the choice is meaningless or harmful.

The cost, stated honestly

Adding a product to the family touches every factory. A fourth collaborator means editing the interface and every implementation. This is the pattern's known weakness, and it is the reason not to reach for it speculatively — the family should be small and stable.

It flattens legitimate variation. If someone genuinely needs cloud blobs with a local queue for a performance experiment, the pattern is now in the way. That is exactly what it was for, so the answer is usually "no" — but it should be a considered no.

It can hide what is actually being constructed. Two levels of indirection between configuration and the object doing the work is one more than most systems need.

Check your understanding

Common mistakes

Grouping by category rather than by consistency. "All the repositories" is a naming scheme; "everything that must speak the same dialect" is a constraint.

A family of one. An Abstract Factory with a single product is a factory with extra steps.

Letting the family grow. Every added product type edits every implementation. Resist adding things that do not need to be consistent.

Exposing the factory to business logic. Services should receive their collaborators, not a factory to fetch them from. The factory belongs at the composition root.

Using it where a single configuration value would do. If the only real requirement is one switch instead of three, a function returning a record of three objects gets you that.

Forgetting that tests are a member of the family. A FakeBackend alongside the real ones is usually the second implementation that justifies the pattern.

Practice

A service reads three environment variables: BLOB_DRIVER, QUEUE_DRIVER and DB_DRIVER, each 'cloud' or 'local'. A postmortem finds an engineer ran with cloud blobs and a local database, and uploaded files became unreferenceable. Design the fix and say what you would do beyond the code change.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

The code change is the easy half. Ask what else was missing for this to reach production.

Show a strong answer

The code change: collapse three decisions into one.

  • Introduce one StorageBackend with three accessors, implemented by CloudBackend and LocalBackend. The service receives the three collaborators, not the backend, so nothing below the composition root knows the pattern exists.
  • Replace the three variables with one, derived from the environment rather than set independently. The old variables should be removed, not defaulted — leaving them readable means the mixed state is still reachable.
  • Fail at start-up on an unknown value. A typo should stop the process, not silently select a branch.

Why this is better than validating the combination: a validator is code that can be skipped, forgotten in a new entry point, or bypassed by a test harness. Removing the ability to express the state removes the class of bug rather than one instance of it.

What I would do beyond the code, because a design change alone does not explain how this reached production:

  • Ask why local drivers were reachable in a production-shaped environment at all. If a deployed process can load an in-memory queue, the packaging is the deeper problem — that ought not to be a runtime decision.
  • Add a start-up assertion that logs the resolved backend by name. The postmortem probably could not tell quickly what the process had been running, and one log line at boot fixes that permanently.
  • Look for the other three-variable decisions. This will not be the only one, and the same shape will fail the same way.
  • Check what recovery looks like. Unreferenceable blobs are still in the bucket; a reconciliation job that lists objects and finds ones with no metadata row both cleans up this incident and detects the next.

What I would not do: add a documentation note saying "these three must match". That is the control that already failed.

A team proposes an Abstract Factory grouping UserRepository, OrderRepository and InvoiceRepository, arguing that this makes swapping to in-memory versions easy for tests. Evaluate the proposal.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Ask what tests actually do today with those three.

Show a strong answer

The stated benefit is real but small, and the cost is a capability they are currently using.

Where the proposal is right: a test that wants everything in memory currently constructs three fakes and must remember all three. A missed one silently reaches a real database, which is a genuine and annoying failure mode. One InMemoryRepositories removes that.

Where it is wrong: these three are not a consistency family. Nothing breaks if a test uses a real UserRepository against a test database and a fake OrderRepository — and that combination is common and useful, because it is how you test a narrow slice against real query behaviour without setting up the world. An Abstract Factory forbids it, and the team will immediately invent an escape hatch, at which point the guarantee is gone and only the indirection remains.

The distinguishing question I would ask them: has a mismatched pair of these ever caused a bug? For a storage backend the answer is yes and the bug is data loss. For three repositories over the same database the answer is almost always no — they share a connection and a transaction, and that shared thing is the real family.

What I would propose instead:

  • A test helper, not a pattern. A function that returns all three fakes gives the convenience with no constraint: const { users, orders, invoices } = inMemoryRepos(). Tests that want a mix simply do not use it.
  • If missing a fake is the actual pain, attack that directly — make the real repositories refuse to construct when a test-environment flag is set. That catches the mistake everywhere, including in tests that do not use the helper.

What would change my mind: if the repositories were to sit over different stores — users in Postgres, orders in a document database — and a transaction had to span them. Then consistency between them becomes a real constraint and the family is genuine.

Key takeaways

  • Abstract Factory exists to make invalid combinations unrepresentable, not to group related classes.
  • The test is "would choosing rightly for one and wrongly for another be a bug?" If not, the products are independent and should stay so.
  • It differs from Factory Method in producing a family via composition, rather than one product via a subclass.
  • Real cases share one shape: partial application of the choice is meaningless or harmful — database dialects, protocol versions, offline test doubles.
  • The known cost is that new product types edit every implementation, so keep the family small and settled.
  • Often one configuration value and a function returning a record delivers most of the benefit; reach for the full pattern when others will extend the family.

Further reading

  • Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
  • Robert C. Martin, Agile Software Development, Principles, Patterns, and Practices (Prentice Hall, 2002).
  • Mark Seemann and Steven van Deursen, Dependency Injection Principles, Practices, and Patterns (Manning, 2019).
  • Scott Wlaschin, Domain Modeling Made Functional (Pragmatic Bookshelf, 2018) — making illegal states unrepresentable.
  • Michael Nygard, Release It!, second edition (Pragmatic Bookshelf, 2018) — configuration as a source of production failure.

Check yourself

3 questions in this lesson. Answer each once to complete it.

No problem practices this lesson yet. Browse the practice problems

Next lessonBuilder
Lessons in this track