Design PatternsCreational
Abstract Factory Pattern
Provide an interface for creating families of related objects without specifying their concrete classes.
Design Patterns
Intent
Abstract Factory groups several factory methods into one object so that a client can create a whole family of related products that are guaranteed to work together — without knowing which family it is using.
Think UI toolkits (a dark button must go with a dark checkbox), cloud providers (an AWS queue with an AWS blob store), or database dialects.
Structure
classDiagram
class CloudFactory {
<<interface>>
+createStorage() Storage
+createQueue() Queue
}
class AwsFactory
class AzureFactory
class Storage {
<<interface>>
}
class Queue {
<<interface>>
}
CloudFactory <|.. AwsFactory
CloudFactory <|.. AzureFactory
Storage <|.. S3Storage
Storage <|.. BlobStorage
Queue <|.. SqsQueue
Queue <|.. ServiceBusQueue
AwsFactory ..> S3Storage
AwsFactory ..> SqsQueue
AzureFactory ..> BlobStorage
AzureFactory ..> ServiceBusQueue
Client --> CloudFactory
Example
interface Storage { void upload(String file); }
interface Queue { void publish(String message); }
// The abstract factory: one method per product in the family
interface CloudFactory {
Storage createStorage();
Queue createQueue();
}
// --- AWS family ---
class S3Storage implements Storage {
public void upload(String file) { System.out.println("S3 ⬆ " + file); }
}
class SqsQueue implements Queue {
public void publish(String msg) { System.out.println("SQS ✉ " + msg); }
}
class AwsFactory implements CloudFactory {
public Storage createStorage() { return new S3Storage(); }
public Queue createQueue() { return new SqsQueue(); }
}
// --- Azure family ---
class BlobStorage implements Storage {
public void upload(String file) { System.out.println("Blob ⬆ " + file); }
}
class ServiceBusQueue implements Queue {
public void publish(String msg) { System.out.println("Bus ✉ " + msg); }
}
class AzureFactory implements CloudFactory {
public Storage createStorage() { return new BlobStorage(); }
public Queue createQueue() { return new ServiceBusQueue(); }
}
// Client code only knows the abstractions
class ReportService {
private final Storage storage;
private final Queue queue;
ReportService(CloudFactory cloud) {
this.storage = cloud.createStorage();
this.queue = cloud.createQueue();
}
void publishReport(String name) {
storage.upload(name + ".pdf");
queue.publish("report-ready:" + name);
}
}
class Main {
public static void main(String[] args) {
String provider = args.length > 0 ? args[0] : "aws";
CloudFactory factory = switch (provider) {
case "azure" -> new AzureFactory();
default -> new AwsFactory();
};
new ReportService(factory).publishReport("q3-revenue");
new ReportService(new AzureFactory()).publishReport("q3-revenue");
}
}Output
The choice of family is made once, at the composition root. Everything downstream is provider-agnostic, and it is impossible to accidentally mix an S3 store with an Azure queue.
Where you’ll see it
javax.xml.parsers.DocumentBuilderFactory,TransformerFactory— pluggable XML implementations- JDBC: a
Connectionacts as a factory for matchingStatement,PreparedStatementandBlobobjects from the same driver - Spring profiles:
@Profile("aws")and@Profile("azure")configuration classes that each define a consistent set of beans are Abstract Factory, wired by the container
Trade-offs
| Pros | Cons |
|---|---|
| Guarantees products from one family are used together | Adding a new product type changes every factory |
| Swapping families is a one-line change | Many interfaces and classes |
| Isolates concrete classes from clients | Overkill when there is only one family |
Finished this lesson?
Mark it complete to track your progress through Design Patterns.