Abstract Factory — Families of Objects That Always Match
Create whole families of related objects without naming a single concrete class, and kill the platform if/else sprawl
Abstract Factory — Families of Objects That Always Match
Create whole families of related objects without naming a single concrete class, and kill the platform if/else sprawl
Photo by Spacejoy on Unsplash
A furniture showroom doesn’t hand you a random chair, a random table, and a random lamp and wish you luck. It sells you a set, where the chair, the table, and the lamp were designed to sit in the same room without clashing. The Abstract Factory pattern does that for objects: it hands your code a whole family of parts that are guaranteed to belong together.
This post covers what Abstract Factory is, how to build it in Java, the trade-offs it carries, and when it tips over into over-engineering. It does not re-explain Factory Method — that one creates a single product and gets its own post — though the two are compared near the end.
What is the Abstract Factory Pattern?
Picture a service that needs cloud infrastructure: somewhere to store files, a queue to pass messages, and a vault to hold secrets. You start on Azure, so you write new AzureBlobStore(), new AzureServiceBusQueue(), and new AzureKeyVault() wherever each is needed. It works. Then the company signs a customer who insists on running in their own AWS account, and now the same three objects need AWS variants - AwsS3Store, AwsSqsQueue, AwsSecretsManager - chosen at startup. The catch is that these three must come from the same provider. An Azure blob store paired with an AWS queue is two sets of credentials, two SDKs, and two regions stitched together - a configuration that fails in confusing ways.
Abstract Factory fixes this. You define one factory interface — CloudFactory - with a method per product: createBlobStore(), createQueue(), createSecretVault(). Each provider gets one concrete factory that hands back its own matching parts. Your service takes a CloudFactory, asks it for the three objects, and works with them through their interfaces. It never names a single concrete class. And since all three come from the same factory, they're always from the same provider - you couldn't mix them if you tried.
Think back to the furniture showroom. The showroom is the abstract factory. The “mid-century” showroom and the “industrial” showroom are concrete factories. The chair, table, and lamp are the products. You ask the showroom for “a chair,” and you get one that matches everything else it sells. You physically cannot walk out with a mid-century chair and an industrial lamp from the same order, which is exactly the guarantee the pattern gives your code.
“Provide an interface for creating families of related or dependent objects without specifying their concrete classes.” — Gang of Four (GoF), Design Patterns: Elements of Reusable Object-Oriented Software
The part that’s easy to miss is where the real risk lives. It was never picking the wrong provider — you’d catch that on the first run. It’s quietly pairing two of them. With concrete classes scattered across the codebase, nothing stops one file from new-ing an Azure object while a file three directories away grabs an AWS one. Abstract Factory takes that off the table. There's one factory in play, and a factory only ever builds its own family.
Examples
Real systems that lean on Abstract Factory -
- One app, native look on every OS — An app that runs on Windows, macOS, and Linux asks a single platform factory for its buttons and menus, and gets controls that look right for whichever OS it is on — never a Windows-style button sitting on a Mac. This is the canonical example from the original GoF book.
- Dark mode and light mode — Flip your phone or editor to dark mode and everything switches together: buttons, menus, scrollbars, all matching. Behind that toggle is one theme factory handing back a full set of “dark” parts, so a bright white button never lands on a dark screen.
- Swapping your database — Point an app at MySQL or PostgreSQL, and the driver hands back a matching set of connection and query objects for that one database. You never accidentally run a MySQL query over a Postgres connection. (This is what JDBC’s
Connectionand .NET'sDbProviderFactoryquietly do under the hood.) - Online checkout with more than one payment provider — A checkout that supports both Stripe and PayPal asks one provider’s factory for its charge client, refund client, and receipt sender. Because all three come from the same factory, you can never try to refund a Stripe charge through PayPal.
I leaned on Abstract Factory once to keep a service cloud-portable. One CloudFactory per provider produced the storage, queue, and secret-store clients, so the exact same business code ran on Azure for the multi-tenant SaaS and on AWS for one customer's isolated deployment. More on that below.
Problems Addressed by Abstract Factory
- Mismatched families slip through — Without the pattern, nothing stops an Azure blob client sitting beside an AWS queue. The first integration test that talks to both fails with a credentials error that points nowhere useful. One factory per family makes the mismatch impossible.
- Platform branching spreads everywhere — Hardcoded
newcalls mean every place that needs storage, a queue, or a vault grows its ownif (provider == "azure"). Add a third provider and you edit a dozen files. The factory localizes that choice to one line at startup. - Swapping an implementation touches everything — When concrete classes are named throughout, replacing the storage client means a find-and-replace across the codebase. Behind an interface, you swap the factory and nothing else moves.
- Tests need a fake family — A test wants in-memory storage, an in-memory queue, and an in-memory vault that share state. An
InMemoryCloudFactoryhands all three out as a matched set, with no production code touched. - The caller shouldn’t know the provider — Order-processing logic should know it has “a queue,” not whether it’s Service Bus or SQS.
The Problem — Scattered Platform Branching
Here’s the infrastructure wiring without the pattern:
public class OrderProcessor {
public OrderProcessor(String provider) {
// #1 - The same branch, repeated for every product
if (provider.equals("AZURE")) {
this.store = new AzureBlobStore();
this.queue = new AzureServiceBusQueue();
this.vault = new AzureKeyVault();
} else if (provider.equals("AWS")) {
this.store = new AwsS3Store();
this.queue = new AwsSqsQueue();
this.vault = new AwsSecretsManager(); // #2 - easy to forget one
}
}
}
- #1 — This same branch reappears in every class that needs infrastructure. Adding Google Cloud means hunting down each copy.
- #2 — Nothing enforces that all three come from one provider. Paste an
AzureKeyVault()into the AWS branch by mistake and it compiles cleanly, then fails at runtime against the wrong credentials.
The processor should ask for a family of infrastructure and use it. It shouldn’t assemble that family by hand, branch by branch.
UML for Abstract Factory

Class diagram for Abstract Factory. Each box is one product type — its interface (blue) and the two provider implementations (green). CloudFactory declares one create method per product type; each concrete factory returns only its own provider's versions, so a family can never be mixed.
Implementing Abstract Factory
Step 1 — Product interfaces
Each product is defined by what it does, never by which cloud provides it.
// #1 - Abstract products: one interface per kind of infrastructure
public interface BlobStore { void put(String key, byte[] data); }
public interface MessageQueue { void send(String message); }
public interface SecretVault { String read(String name); }
- #1 — All business code depends on these three interfaces. No concrete provider class appears in the consuming code.
Step 2 — Concrete products, one set per provider
// #1 - The Azure family
public class AzureBlobStore implements BlobStore {
public void put(String key, byte[] data) { /* Azure Blob SDK */ }
}
public class AzureServiceBusQueue implements MessageQueue {
public void send(String message) { /* Service Bus SDK */ }
}
// #2 - The AWS family mirrors it, member for member
public class AwsS3Store implements BlobStore {
public void put(String key, byte[] data) { /* S3 SDK */ }
}
public class AwsSqsQueue implements MessageQueue {
public void send(String message) { /* SQS SDK */ }
}
- #1, #2 — Each provider supplies a full set. Every interface has exactly one implementation per family, so the families stay symmetric. The
SecretVaultimplementations -AzureKeyVaultandAwsSecretsManager- follow the same shape and are left out here only to keep the snippet short.
Step 3 — The abstract factory and its concrete factories
// #1 - The abstract factory: one create method per product
public interface CloudFactory {
BlobStore createBlobStore();
MessageQueue createQueue();
SecretVault createSecretVault();
}
// #2 - A concrete factory returns only its own provider's parts
public class AzureFactory implements CloudFactory {
public BlobStore createBlobStore() { return new AzureBlobStore(); }
public MessageQueue createQueue() { return new AzureServiceBusQueue(); }
public SecretVault createSecretVault() { return new AzureKeyVault(); }
}
- #1 — The factory interface is the contract the whole application codes against.
- #2 —
AzureFactoryis the only place Azure concrete classes are named.AwsFactorymirrors it for AWS. A factory cannot accidentally return another provider's product.
Step 4 — Selecting the family once, in the composition root
// #1 - The composition root: the DI setup (Spring @Configuration, .NET
// Program.cs, or main) where the object graph is wired at startup
CloudFactory factory = switch (config.provider()) {
case "AZURE" -> new AzureFactory();
case "AWS" -> new AwsFactory();
default -> throw new IllegalArgumentException("Unknown provider");
};
// #2 - Register it once as a singleton, then inject that one instance
// into every class that needs infrastructure
OrderProcessor orders = new OrderProcessor(factory);
ShippingService shipping = new ShippingService(factory);
- #1 — This is the only place a concrete factory is named, and it belongs in the composition root — the DI configuration that assembles the app (a Spring
@Configurationbean, a .NETProgram.csregistration, or plainmain). Everything else depends on theCloudFactoryinterface and never seesAzureFactoryorAwsFactory. - #2 — The factory is registered as a singleton: built once and injected into every collaborator, so
OrderProcessorandShippingServiceshare the same instance and can never land on different clouds. Because they all take aCloudFactory, swapping to anInMemoryFactoryfor tests is a one-line change here.
Step 5 — How a DI container expresses this
In a real app you rarely new the factory by hand. A DI container registers it once as a singleton, and every class that declares a CloudFactory parameter gets that same instance. Here's the same composition-root decision in both ecosystems.
Java (Spring):
@Configuration
public class CloudConfig {
// #1 - A @Bean is a singleton by default, so the switch runs once
@Bean
CloudFactory cloudFactory(AppConfig config) {
return switch (config.provider()) {
case "AZURE" -> new AzureFactory();
case "AWS" -> new AwsFactory();
default -> throw new IllegalArgumentException("Unknown provider");
};
}
}
C# (.NET, Program.cs):
// #2 - AddSingleton keeps one CloudFactory for the app's lifetime
builder.Services.AddSingleton<CloudFactory>(sp =>
{
var config = sp.GetRequiredService<AppConfig>();
return config.Provider switch
{
"AZURE" => new AzureFactory(),
"AWS" => new AwsFactory(),
_ => throw new ArgumentException("Unknown provider")
};
});
- #1 — Spring beans are singletons by default, so the provider
switchexecutes once at startup and the resultingCloudFactoryis shared. Any constructor that asks for aCloudFactoryreceives it - nonew, no provider check. - #2 —
AddSingletonis the .NET equivalent: one instance resolved from config and injected everywhere. Both idioms put the single branch inside the container's registration, which is exactly where the composition root lives.
My Use Case — One Codebase, Two Clouds
The service was a document-processing pipeline running as multi-tenant SaaS on Azure. It pulled uploaded files from blob storage, pushed each one onto a queue for background work, and read third-party API keys from a secret vault — three infrastructure clients, all Azure. Then a regulated customer signed on with one non-negotiable condition: the pipeline had to run inside their AWS account, on their network, so their documents never crossed into infrastructure they didn’t own. The same three clients suddenly needed AWS versions, chosen per deployment.
I didn’t want two forks of the pipeline. The actual work — parse, transform, index — was identical across both clouds; only the infrastructure clients differed. So I put a CloudFactory in front of them. The Azure deployment wired in an AzureFactory, the customer's deployment wired in an AwsFactory, and startup read a single config value to pick the family. Everything downstream took a BlobStore, a MessageQueue, and a SecretVault, and never learned which cloud it was standing on.
Two things made it pay off. First, the matching guarantee: I could never ship a build that read files from Azure Blob but published events to SQS, because the factory made that pairing impossible to assemble in the first place. Second, testing got cheap. An InMemoryFactory handed the pipeline a fake storage, queue, and vault backed by a shared HashMap, so the whole flow ran in a plain unit test with no cloud in sight.
The cost showed up later. When the customer needed a capability AWS exposed but Azure didn’t, the symmetric CloudFactory interface had nowhere to put it - I ended up leaking a provider-specific call behind a capability check, which is exactly the strain this pattern warns about. For two well-matched providers it was clearly the right call. A third, more divergent provider would have pushed me toward a different design.

One config value picks the family at startup. The same pipeline runs on either cloud because it only ever touches the product interfaces, never a concrete client.
When to Use Abstract Factory
- You have families of objects that must stay consistent — storage, queue, and vault from one provider; a button, checkbox, and scrollbar from one theme. The matching constraint is the signal.
- The family is chosen once and used widely — selected at startup from config, then threaded through the app.
- You want implementations to be swappable — a real cloud family in production, an in-memory family in tests, behind the same interfaces.
- The concrete classes shouldn’t leak — business logic codes against
BlobStore, neverAzureBlobStore.
You don’t need Abstract Factory when there’s only one family and no credible second one coming. If you’ll only ever run on Azure, the extra interfaces and factories are pure overhead — new AzureBlobStore() is honest and clear. And if you only ever create one product rather than a matched set, you want Factory Method, not this.
Abstract Factory vs. Factory Method
These two get confused constantly, because both end in “factory” and both hide new. The difference is how many products and how they're built.
- Factory Method creates one product, using inheritance. A Creator class declares a method; a subclass overrides it to decide the single concrete type. Covered in its own post.
- Abstract Factory creates a family of related products, using composition. A factory object is passed in, and it produces several matching products that are meant to be used together.
A clean way to remember it: Factory Method answers “which class should I create?” for one object. Abstract Factory answers “which set of classes should I create?” for several that must agree. In fact, the create methods inside an abstract factory are often Factory Methods themselves — Abstract Factory is Factory Method scaled up to a family.
Abstract Factory vs. Strategy
This pairing trips people up less often than the Factory Method mix-up, but it’s sneakier, because the two are almost identical on paper. Both hand your client an interface through the constructor and let you swap the implementation — a CloudFactory injected into a service is wired exactly like a SortStrategy injected into a sorter. The structure matches; the intent does not.
- Strategy is behavioral: the object you inject does the work. You call it and it runs an algorithm —
strategy.compress(file),strategy.price(cart). Covered in its own post. - Abstract Factory is creational: the object you inject makes other objects. You call it to get a family —
factory.createBlobStore()- and those products do the work, not the factory.
So the picking question is simple: does the injected thing perform the operation, or hand you objects that do? If cloud.store(file) writes the file, that's a Strategy - a provider strategy. If cloud.createBlobStore() returns a store you then call, that's Abstract Factory. They even nest cleanly: a concrete factory is, in a sense, a strategy for building a family, which is why swapping AzureFactory for AwsFactory feels so much like swapping a strategy.
Issues with Abstract Factory
Adding a product ripples through every factory — The moment you add a fourth product, say a Cache, you must add createCache() to the factory interface and implement it in every concrete factory. With three providers, one new product is four edits. The pattern is rigid in this exact direction, and it's the most common reason it hurts.
Class count explodes — Every product is an interface plus one implementation per family, and every family needs its own factory. Two products across three providers already puts you at twelve types. The structure stays clean; the file tree does not stay small.
It assumes symmetric families — Abstract Factory works when every family has the same members. If AWS offers a capability Azure doesn’t, the interface can’t express it without leaking provider-specific methods, which defeats the abstraction. Real cloud SDKs diverge, and that divergence is where the pattern strains.
It guarantees consistency at construction, not at runtime — The factory ensures all three clients come from one provider, but it says nothing about how they behave. Azure Blob and S3 differ in their consistency and latency semantics; Service Bus and SQS differ in ordering and delivery guarantees. Code that passes every test against an InMemoryFactory can still misbehave on one cloud, because the interface hides exactly the runtime differences that bite you in production. The matching guarantee is real, but it's narrower than it looks.
Indirection costs readability — A newcomer reading factory.createQueue() can't see which queue they got without tracing how the factory was wired at startup. The decoupling you gained is paid for in a longer path from call to concrete class.
Summary
- Abstract Factory is a creational pattern (one of the five GoF patterns about object creation) that produces families of related objects through one factory interface.
- Its core guarantee is consistency: because one concrete factory builds the whole family, members can never be mismatched.
- It replaces scattered platform branching with a single provider choice made once at startup.
- It uses composition — a factory object is passed to the client — which distinguishes it from Factory Method’s inheritance and single product.
- The main cost is rigidity: adding a new product type forces a change to the factory interface and every concrete factory.
- Reach for it when you have matched families and a real second implementation; skip it when there’s one stable family that won’t grow.
Conclusion
Abstract Factory earns its keep when the hard part isn’t choosing an implementation but keeping a set of them in lockstep. Route every object in a family through one factory, and “remember to keep these consistent” stops being a rule you police by hand. It turns into a guarantee the type system makes for you.
That guarantee isn’t free. You pay with more interfaces, more classes, and a factory contract that’s painful to extend — every new product type touches every factory. For a single family that will never have a sibling, it’s ceremony you don’t need. But when families are real and must stay aligned — cloud providers, UI themes, database backends — Abstract Factory keeps the matching automatic and the concrete classes out of sight. If you’ve followed this series, you’ve seen Singleton, Builder, Prototype, and Factory Method. Abstract Factory completes the creational set by answering the one question the others don’t: how do you create a whole family at once, without letting it fall out of step?
Written & edited by — Vivek Mittal
https://www.linkedin.com/in/vivekmittal06/
Additional Reading
메타데이터
- post_id
- f3602b4c7d6d
- slug
- abstract-factory-families-of-objects-that-always-match-f3602b4c7d6d
- url
- https://levelup.gitconnected.com/abstract-factory-families-of-objects-that-always-match-f3602b4c7d6d
- canonical_url
- https://levelup.gitconnected.com/abstract-factory-families-of-objects-that-always-match-f3602b4c7d6d
- author_url
- https://medium.com/@stellarsoftwareco
- status
- ok
- fetched_at
- 2026-07-29 11:52:52