Provide an interface for creating families of related objects without specifying their concrete classes.
01
The problem Abstract Factory solves
The problem
Your service needs three infrastructure pieces: object storage, a message queue, and a secrets store. On AWS that's S3, SQS and Secrets Manager. In local development it's MinIO, an in-memory queue, and a .env file.
So each one gets its own factory, and the wiring code picks three implementations independently. Which works right up until someone configures S3Storage with InMemoryQueue — a combination that compiles, starts up, passes the smoke test, and then loses every message on restart in staging.
The individual choices were all valid. The combination wasn't, and nothing in the code could say so.
02
How the Abstract Factory pattern works
Stop choosing the pieces separately. Choose the family once, and let it hand you every member:
Define an abstract factory with one creation method per product: createStorage(), createQueue(), createSecrets().
Each concrete factory implements all of them with one consistent family — AwsFactory returns S3, SQS and Secrets Manager and nothing else.
The application picks a factory exactly once, at startup, and asks it for everything. Mixing families becomes unrepresentable.
The difference from Factory Method, precisely. Factory Method makes one product and varies it by inheritance — a subclass overrides the creation method. Abstract Factory makes a family and varies it by composition — you hold a factory object and ask it for each member. In practice an Abstract Factory is usually implemented as several Factory Methods on one interface, which is why the two get conflated.
Participants. The Abstract Factory declares one creation method per product type. Each Concrete Factory supplies one internally-consistent family. Abstract Products are the interfaces the application codes against. The client holds a factory, never a concrete product class — so the colours can never mix.
03
See it: one choice, a whole matched family
Pick an environment. Each factory hands back all three pieces at once, from one family — there is no path through this that produces S3 plus an in-memory queue.
▶ Try it — pick a family, not a piece
An interactive selector: choosing an environment selects one concrete factory, which supplies a matched set of storage, queue and secrets implementations — mixing implementations across families is not possible.
Press Add a new environment to bring GCP online. Adding a whole cloud is one new class implementing three methods — and the application code that uses storage, queues and secrets is untouched. That's the payoff, and it's also the cost: see the pitfall below about adding a fourth product.
04
Abstract Factory code examples
One factory per environment, each supplying a consistent infrastructure family.
public interface Storage { void put(String key, byte[] data); }
public interface Queue { void publish(Message m); }
public interface Secrets { String get(String name); }
// The abstract factory: one creation method per product in the family.
public interface InfraFactory {
Storage createStorage();
Queue createQueue();
Secrets createSecrets();
}
public final class AwsFactory implements InfraFactory {
public Storage createStorage() { return new S3Storage(region); }
public Queue createQueue() { return new SqsQueue(region); }
public Secrets createSecrets() { return new SecretsManagerSecrets(region); }
}
public final class LocalFactory implements InfraFactory {
public Storage createStorage() { return new MinioStorage("http://localhost:9000"); }
public Queue createQueue() { return new InMemoryQueue(); }
public Secrets createSecrets() { return new DotEnvSecrets(Path.of(".env")); }
}
// Chosen ONCE, at the edge of the application:
InfraFactory infra = switch (env) {
case "prod" -> new AwsFactory();
case "local" -> new LocalFactory();
default -> throw new IllegalArgumentException(env);
};
var app = new App(infra.createStorage(), infra.createQueue(), infra.createSecrets());
// App only ever sees Storage, Queue and Secrets. There is no expression
// anywhere in the codebase that yields S3Storage next to an InMemoryQueue.
from typing import Protocol
class Storage(Protocol):
def put(self, key: str, data: bytes) -> None: ...
class Queue(Protocol):
def publish(self, message: Message) -> None: ...
class Secrets(Protocol):
def get(self, name: str) -> str: ...
class InfraFactory(Protocol):
def create_storage(self) -> Storage: ...
def create_queue(self) -> Queue: ...
def create_secrets(self) -> Secrets: ...
class AwsFactory:
def __init__(self, region: str) -> None:
self._region = region
def create_storage(self) -> Storage:
return S3Storage(self._region)
def create_queue(self) -> Queue:
return SqsQueue(self._region)
def create_secrets(self) -> Secrets:
return SecretsManagerSecrets(self._region)
# Python doesn't need `implements`: AwsFactory satisfies InfraFactory
# structurally. A NamedTuple bundle is often even more direct, since the
# "factory" here has no behaviour beyond construction:
#
# class Infra(NamedTuple):
# storage: Storage
# queue: Queue
# secrets: Secrets
#
# def aws(region: str) -> Infra:
# return Infra(S3Storage(region), SqsQueue(region), SecretsManagerSecrets(region))
#
# Same guarantee — you still can't build a mixed family — with one function
# per environment instead of one class.
public interface IStorage { Task PutAsync(string key, byte[] data); }
public interface IQueue { Task PublishAsync(Message message); }
public interface ISecrets { Task<string> GetAsync(string name); }
public interface IInfraFactory
{
IStorage CreateStorage();
IQueue CreateQueue();
ISecrets CreateSecrets();
}
public sealed class AwsFactory(string region) : IInfraFactory
{
public IStorage CreateStorage() => new S3Storage(region);
public IQueue CreateQueue() => new SqsQueue(region);
public ISecrets CreateSecrets() => new SecretsManagerSecrets(region);
}
// In .NET the DI container usually plays this role — but note that registering
// the three interfaces separately loses the family guarantee:
//
// services.AddSingleton<IStorage, S3Storage>(); // nothing stops you
// services.AddSingleton<IQueue, InMemoryQueue>(); // from mixing here
//
// Registering the FACTORY keeps it:
//
// services.AddSingleton<IInfraFactory>(_ => env switch {
// "prod" => new AwsFactory(region),
// "local" => new LocalFactory(),
// });
// services.AddSingleton(sp => sp.GetRequiredService<IInfraFactory>().CreateStorage());
//
// That's the whole argument for using this pattern alongside a container.
// A factory here is an object of constructor functions — no classes needed.
const awsFactory = region => ({
createStorage: () => new S3Storage(region),
createQueue: () => new SqsQueue(region),
createSecrets: () => new SecretsManagerSecrets(region),
})
const localFactory = () => ({
createStorage: () => new MinioStorage('http://localhost:9000'),
createQueue: () => new InMemoryQueue(),
createSecrets: () => new DotEnvSecrets('.env'),
})
const FACTORIES = {
prod: () => awsFactory(process.env.AWS_REGION),
local: localFactory,
}
// Chosen once, at the composition root.
const makeInfra = FACTORIES[process.env.APP_ENV]
if (!makeInfra) throw new Error(`unknown APP_ENV: ${process.env.APP_ENV}`)
const factory = makeInfra()
export const infra = {
storage: factory.createStorage(),
queue: factory.createQueue(),
secrets: factory.createSecrets(),
}
// The whole app imports `infra` and gets a consistent set. Nothing in the
// codebase can construct a half-AWS, half-local one.
package infra
// Go's implicit interfaces make the abstract factory a plain interface with no
// declaration ceremony — and because a struct of interfaces is just as good
// here, many Go codebases skip the factory type entirely.
type Storage interface{ Put(key string, data []byte) error }
type Queue interface{ Publish(Message) error }
type Secrets interface{ Get(name string) (string, error) }
type Factory interface {
Storage() Storage
Queue() Queue
Secrets() Secrets
}
type awsFactory struct{ region string }
func AWS(region string) Factory { return awsFactory{region} }
func (f awsFactory) Storage() Storage { return NewS3(f.region) }
func (f awsFactory) Queue() Queue { return NewSQS(f.region) }
func (f awsFactory) Secrets() Secrets { return NewSecretsManager(f.region) }
// The flatter, very common Go alternative — a single constructor per family
// returning a bundle. Same guarantee, no interface needed:
//
// type Infra struct {
// Storage Storage
// Queue Queue
// Secrets Secrets
// }
//
// func NewAWS(region string) Infra {
// return Infra{NewS3(region), NewSQS(region), NewSecretsManager(region)}
// }
//
// Prefer the interface form only when the family must be swapped at runtime
// rather than chosen once at startup.
interface Storage { put(key: string, data: Uint8Array): Promise<void> }
interface Queue { publish(message: Message): Promise<void> }
interface Secrets { get(name: string): Promise<string> }
interface InfraFactory {
createStorage(): Storage
createQueue(): Queue
createSecrets(): Secrets
}
class AwsFactory implements InfraFactory {
constructor(private readonly region: string) {}
createStorage(): Storage { return new S3Storage(this.region) }
createQueue(): Queue { return new SqsQueue(this.region) }
createSecrets(): Secrets { return new DotEnvSecrets() }
// ^^^^^^^^^^^^^^^^^^ compile error:
// DotEnvSecrets is not assignable to Secrets in the AWS family — and even
// when it structurally matches, keeping each family in its own class is what
// makes a mismatched member reviewable in a diff.
}
const FACTORIES = {
prod: () => new AwsFactory(process.env.AWS_REGION!),
local: () => new LocalFactory(),
} satisfies Record<string, () => InfraFactory>
type Env = keyof typeof FACTORIES // 'prod' | 'local'
export function makeInfra(env: Env) {
const f = FACTORIES[env]()
return { storage: f.createStorage(), queue: f.createQueue(), secrets: f.createSecrets() }
}
// `satisfies` keeps Env as a literal union, so an unknown environment name is
// a compile error rather than an undefined at startup.
Read across the tabs: Python and Go both point out that a plain bundle — a NamedTuple or a struct of interfaces, built by one function per family — gives the identical guarantee with far less machinery. The full factory interface earns its place when the family must be swapped at runtime, not merely chosen at startup. The C# tab shows the trap worth remembering in any DI codebase: registering the three products separately quietly throws the family guarantee away.
05
How to implement Abstract Factory
Write down the matrix: products across the top, families down the side. If it isn't a full grid — some families genuinely lack some products — this pattern will fight you.
Define an abstract product interface per column.
Define the abstract factory with one creation method per column.
Implement one concrete factory per row, returning only that family's classes.
Choose the factory exactly once, at the composition root. Every additional selection site is another chance to mix families.
Make sure the application only ever names the abstract products. A single new S3Storage() deep in the code defeats the whole thing.
06
When to use Abstract Factory — and when not to
Use it when several objects must come from the same family to be correct, when you support multiple backends or platforms, or when tests need a wholesale swap to fakes. The signal is a combination that compiles but is wrong.
Where it goes wrong
Adding a product is expensive. Adding a family is cheap — one new class. Adding a fourth product means changing the factory interface and every concrete factory. If your products change more often than your families, you have the matrix the wrong way round and this pattern is actively costly.
The family that isn't square. When the local family has no secrets manager, you get a stub throwing UnsupportedOperation — a hole in the abstraction that shows up at runtime, in production, on the one path nobody tested.
Applied at one family. An abstract factory with a single implementation is three interfaces and a class doing what a constructor already did. Wait for the second backend.
Leaky concrete types. The guarantee only holds while the application code speaks in abstract products. One cast to S3Storage to reach a bucket-specific method and you're coupled to AWS again.
A struct or NamedTuple bundle gives the same guarantee with less code.
07
Quick check
🧠 Quick check
You have InfraFactory with three creation methods and four concrete factories. Now you need a fourth product, a cache. What does that cost?
This is the central trade-off. Adding a family is cheap: one new class implementing the existing interface, and nothing else moves. Adding a product is expensive: the interface grows a method and every concrete factory must implement it. Abstract Factory is the right shape when families change more often than products — and the wrong shape, sometimes badly so, when it's the other way round.
In the wild
JavaDocumentBuilderFactory and TransformerFactory in JAXP — pick a factory, get a matched parser toolchain.
C#DbProviderFactory — one factory yields a matching connection, command and parameter set, so you can't pair a SQL Server command with a Postgres connection.
Pythonmultiprocessing.get_context("spawn") returns a context whose Queue, Lock and Process all work together — an abstract factory in the standard library.
Gocrypto/tls cipher-suite selection, and the common "one constructor per environment returning a struct of interfaces" wiring idiom.
C++Qt's QStyleFactory, producing a widget style whose components are consistent with each other.
TypeScriptTesting setups that swap an entire adapter family — real HTTP, storage and clock in production, fakes in tests — behind one factory.
What is the difference between Abstract Factory and Factory Method?
Factory Method creates one product and varies it through inheritance: a subclass overrides the creation method. Abstract Factory creates a family of related products and varies it through composition: you hold a factory object and ask it for each member. An Abstract Factory is typically implemented as several Factory Methods on one interface, which is why they're so often confused. Ask yourself whether your problem is "which class do I instantiate" (Factory Method) or "these objects must all come from the same set" (Abstract Factory).
When is Abstract Factory the wrong choice?
When products change more often than families. Adding a family costs one class; adding a product costs a change to the interface and to every concrete factory. If you're constantly adding product types across a stable set of two backends, that trade is backwards and a registry or DI container will serve you better. It's also wrong when the matrix isn't full — a family missing a product forces stub implementations that throw at runtime.
Does a DI container replace Abstract Factory?
Partly, and you should be careful about the part it doesn't. A container removes the coupling to concrete classes, which is most of the benefit. But registering each interface separately loses the family guarantee — nothing stops a configuration that pairs real S3 storage with an in-memory queue. Registering the factory itself, and resolving the products from it, keeps both.
Is Abstract Factory only for cross-platform UI toolkits?
That's the original GoF example, and it's the least relevant one today. The pattern is alive and well anywhere a set of collaborating implementations must agree: cloud provider abstractions, database driver families, real-versus-fake infrastructure in tests, and multi-tenant deployments where a tenant's storage, queue and identity provider are chosen together.
Finish the handbook, earn the certificate
All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.