LESSON 4.4
Abstract Factory
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
13 min read
Real-world analogy
Imagine buying a coordinated set of living room furniture. You want a chair, a sofa, and a coffee table. If you purchase a Victorian ornate wooden chair, a modern minimalist chrome sofa, and an industrial steel coffee table, your room looks mismatched and incoherent.
An Abstract Factory is like a specialized furniture design house: you go to the Victorian Furniture Studio to get Victorian chairs, sofas, and tables that belong together, or you visit the Modern Scandinavian Studio to get sleek, matching contemporary pieces. You never mix products from incompatible design families.
The problem
Consider a multi-cloud deployment orchestrator or a cross-platform user interface toolkit. If your application code directly instantiates AWS EC2 instances alongside Google Cloud Storage buckets, the system suffers from architectural mismatch: authentication tokens, VPC networking semantics, and region identifiers fail to integrate cleanly.
The naive code below shows what happens when family instantiations are scattered across caller logic:
class DeploymentService {
deploy(provider, config) {
let compute, storage;
// Callers mix and match incompatible components by mistake!
if (provider === "aws") {
compute = new AwsVirtualMachine();
storage = new AwsBucket();
} else if (provider === "gcp") {
compute = new GcpComputeInstance();
storage = new GcpCloudBucket();
} else {
throw new Error(`Unsupported provider: ${provider}`);
}
return `${compute.provision(config)} & ${storage.allocate(config)}`;
}
}
class AwsVirtualMachine { provision(c) { return `AWS EC2 (${c.name})`; } }
class AwsBucket { allocate(c) { return `AWS S3 Bucket (${c.name})`; } }
class GcpComputeInstance { provision(c) { return `GCP Compute (${c.name})`; } }
class GcpCloudBucket { allocate(c) { return `GCP Cloud Storage (${c.name})`; } }
const service = new DeploymentService();
console.log(service.deploy("aws", { name: "analytics-prod" }));public class Example {
static class AwsCompute { String provision(String name) { return "AWS EC2 (" + name + ")"; } }
static class AwsStorage { String allocate(String name) { return "AWS S3 (" + name + ")"; } }
static class GcpCompute { String provision(String name) { return "GCP Compute (" + name + ")"; } }
static class GcpStorage { String allocate(
The pain is severe: when Azure or on-premises support is introduced, every single deployment method across the entire codebase must be updated with additional branching logic. Furthermore, there is nothing stopping a junior developer from pairing an AwsVirtualMachine with a GcpCloudBucket, creating an incompatible hybrid configuration.
The solution
Intent: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
The participants are:
- AbstractFactory: Declares creation methods for each abstract product type in the family (
createCompute(),createStorage()). - ConcreteFactory: Implements creation methods returning concrete product instances belonging to a specific family variant (
AwsFactory,GcpFactory). - AbstractProduct: Declares the domain interface for a type of product (
Compute,Storage). - ConcreteProduct: Implements the product interface for a specific family (
AwsCompute,GcpCompute). - Client: Interacts only with the abstract factory and abstract products, completely isolated from vendor-specific classes.
Step-by-step implementation
Step 1: Define Abstract Products
Declare the interfaces for the individual components that comprise a family.
class Compute {
launch(spec) { throw new Error("Not implemented"); }
}
class Storage {
store(data) { throw new Error("Not implemented"); }
}public class Example {
interface Compute {
Step 2: Declare the Abstract Factory interface
Define the factory interface containing creation methods for every product in the family.
class CloudFactory {
createCompute() { throw new Error("createCompute() must be implemented"); }
createStorage() { throw new Error("createStorage() must be implemented"); }
}public class Example {
interface Compute { String launch(String spec); }
interface Storage { String store(String data); }
Step 3: Implement concrete families
Write concrete product classes and matching concrete factories for each target environment.
The complete example
Here is a runnable cross-cloud infrastructure provisioning service. The client accepts an abstract CloudFactory and builds a complete, guaranteed-compatible cluster without importing a single provider-specific class.
EC2 VM [web-api] provisioned in us-east-1
S3: uploaded 'bundle.tar.gz' with AES-256
GCP Compute Engine [worker-node] in us-central1
GCS Bucket: saved 'dataset.parquet' multi-regionalVariations
1. Families of products: UI toolkits
The classic GoF example. An abstract GUIFactory defines createButton() and createCheckbox(). Concrete factories MacFactory and WindowsFactory produce platform-styled widgets, guaranteeing that a Mac button is never styled alongside a Windows checkbox.
2. Multi-tenant database dialects
An ORM or SQL engine uses an abstract DatabaseDialectFactory producing matching QueryBuilder, SchemaInspector, and ConnectionDriver implementations for PostgreSQL, MySQL, and Oracle.
3. Factory Method vs Abstract Factory
The distinction is a favorite interview topic:
- Factory Method: Uses inheritance. An abstract class defines a single method returning a single product. Subclasses override the method.
- Abstract Factory: Uses object composition. An interface defines multiple creation methods returning a family of products. Clients receive a factory instance and call its methods.
Where you've already used it
- Java XML and DOM parsing (JAXP):
javax.xml.parsers.DocumentBuilderFactory.newInstance(): Returns a concrete factory capable of building matching XML parsers, validators, and document models.javax.xml.transform.TransformerFactory.newInstance(): Coordinates related XML transformation processors and serializers.
- JDBC database drivers:
- A
java.sql.Driveracts as an abstract factory producing compatibleConnection,Statement, andResultSetimplementations for a specific SQL engine.
- A
- Spring Framework Resource Loaders:
- Spring's
ResourceLoaderfamily produces environment-specificResource,InputStreamSource, and file descriptors for filesystems, classpaths, or cloud buckets.
- Spring's
Pros and cons
| Strengths | Weaknesses |
|---|---|
| Guarantees product compatibility within a family | Difficult to extend the product suite (adding a product requires editing all factories) |
| Decouples client code from concrete vendor implementations | Heavyweight pattern with substantial interface overhead |
| Enforces Single Responsibility (family creation isolated) | Increases initial system complexity for small applications |
| Enforces Open/Closed Principle for new families | Can lead to code duplication across parallel factory hierarchies |
When not to use it
Do not use Abstract Factory if you are creating single, standalone objects (use Factory Method or Simple Factory instead). Only use Abstract Factory when your domain inherently features families of interrelated products that must be guaranteed compatible at runtime.
Related patterns
| Pattern | Relationship to Abstract Factory |
|---|---|
| Factory Method | Abstract Factory implementations are commonly implemented as a collection of Factory Methods. |
| Singleton | Concrete factories are usually implemented as singletons since one factory instance is enough per platform. |
| Builder | Builder focuses on step-by-step construction of one complex product; Abstract Factory focuses on creating families of simple products immediately. |
| Prototype | An Abstract Factory can store a set of prototype product instances and clone them rather than subclassing creators. |
Abstract Factory: cloud resources
Refactor cloud resource provisioning from conditional branching on provider names into the Abstract Factory pattern, guaranteeing compatible families of compute and storage resources.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
Q1: How do you add a new product type (e.g., Cache) to an existing Abstract Factory?
This is the primary weakness of the pattern: you must update the CloudFactory interface to add createCache(), which forces updates across all concrete factories (AwsFactory, GcpFactory). If products change frequently, consider a prototype-based or registry-based factory.
Q2: How do you select which Concrete Factory to instantiate at startup?
Typically using a simple factory, dependency injection configuration, or environment variable reading (process.env.CLOUD_PROVIDER). Once initialized, the chosen concrete factory is injected into client classes as a singleton.
Q3: Can an Abstract Factory return singletons?
Yes. Products created by an abstract factory can themselves be singletons or pool references (such as connection pools or shared token caches) if the concrete factory caches them internally.
Q4: What happens if a client accidentally mixes products from two different concrete factories?
The Abstract Factory pattern prevents this by design. Because client constructors accept the factory interface, the client only calls methods on that single factory, ensuring every product instance comes from the same consistent family.
Try it
Connection Pool LLD
Manage bounded resources, acquisition deadlines and safe lease return under concurrency.