Mastering the Abstract Factory: One Interface to Create Them All
Go beyond single object creation and learn how the Abstract Factory pattern helps you create families of related shipping objects, ensuring consistency in your logistics software.
This article had its first draft written by AI, then refined by human editing to ensure clarity, correctness, and real-world applicability.
1. The Hook and Introduction
In our last post we used a fast-food franchise as an analogy for the Factory Method: each franchise (subclass) decides which burger (product) to make. But what about a combo meal? A combo isn’t just a random burger, fries, and drink — it’s a consistent family of items that go together (e.g., Classic Beef Burger + Classic Fries + Soda).
The Abstract Factory pattern lifts the Factory Method one level higher: instead of a factory that creates a single product, you build a factory that produces an entire family of related products (a full meal). That guarantees compatibility between the components of the family — exactly what you want when the pieces must work together.
In this article, you’ll learn:
- What the Abstract Factory pattern is and how it extends the Factory Method.
- How to model a logistics example so selecting “Sea Logistics” yields a consistent shipping kit.
- A practical Java implementation you can adapt to your codebase.
- The key trade-offs so you’ll know when to use it (and when not to).
2. A Real-World Analogy: The Logistics Company Revisited
Recall the Logistics creator from the Factory Method article that produced a single Transport object (e.g., Truck or Ship). Real shipments, however, are composed of multiple cooperating pieces:
- Sea Freight Family:
Ship+Container+... - Road Freight Family:
Truck+CardboardBox+...
If you mix and match (e.g., a Container with a CardboardBox-based manifest), things break. The Abstract Factory enforces that when you pick Sea Logistics, you receive only Sea products — a consistent shipping kit.
3. Pattern Overview
The Gang of Four describe the Abstract Factory as:
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
Core roles:
- Abstract Factory (ShippingKitFactory) — declares creation methods for each product in the family (e.g.,
createTransport(),createPackaging(),...). - Concrete Factory (SeaShippingKitFactory, RoadShippingKitFactory) — implements the abstract factory and produces concrete products that belong together.
- Abstract Product (Transport, Packaging, …) — interfaces for each product type.
- Concrete Product (Ship, Container, Truck, CardboardBox, …) — platform-specific implementations.
- Client (ShippingManager) — uses abstract factory and product interfaces; it never depends on concrete classes.
4. UML Diagram
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+---------------------+ +-------------+ +-------------+
| <interface> | | <interface> | | <interface> |
| ShippingKitFactory | | Transport | | Packaging |
+---------------------+ +-------------+ +-------------+
| + createTransport() | | + deliver() | | + pack() |
| + createPackaging() | +-------------+ +-------------+
+---------------------+ ^ ^ ^ ^
^ ^ | | | |
| | | | | |
| | | | | |
| +-----------------------+ | +-------------+ | +-----------+
| | SeaShippingKitFactory | +----+-->| Ship | +---+-->| Container |
| +-----------------------+ | | +-------------+ | | +-----------+
| | + createTransport() |----------+ | | + deliver() | | | | + pack() |
| | + createPackaging() |----------+ | +-------------+ | | +-----------+
| +-----------------------+ | | | |
| +----+---------------------+ |
+------------------------+ | |
| RoadShippingKitFactory | +-----------------+ +----------+
+------------------------+ +--->| Truck | +-->| Box |
| + createTransport() |------------------+ +-----------------+ | +----------+
| + createPackaging() |------------------+ | + deliver() | | | + pack() |
+------------------------+ | +-----------------+ | +----------+
| |
+--------------------------+
5. Code Example in Java
Below is a compact, but complete, example showing how to implement the pattern for our logistics scenario.
Step 1 — Abstract Product Interfaces
1
2
3
4
5
6
7
8
9
// Abstract Product A: Transport
public interface Transport {
void deliver(String destination);
}
// Abstract Product B: Packaging
public interface Packaging {
void pack(String items);
}
Step 2 — Concrete Product Implementations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// --- Sea Freight Family ---
public class Ship implements Transport {
@Override
public void deliver(String destination) {
System.out.println("Delivering goods by SEA to " + destination);
}
}
public class Container implements Packaging {
@Override
public void pack(String items) {
System.out.println("Packing " + items + " into a steel container.");
}
}
// --- Road Freight Family ---
public class Truck implements Transport {
@Override
public void deliver(String destination) {
System.out.println("Delivering goods by ROAD to " + destination);
}
}
public class CardboardBox implements Packaging {
@Override
public void pack(String items) {
System.out.println("Packing " + items + " into cardboard boxes.");
}
}
Step 3 — Abstract Factory Interface
1
2
3
4
public interface ShippingKitFactory {
Transport createTransport();
Packaging createPackaging();
}
Step 4 — Concrete Factories
1
2
3
4
5
6
7
8
9
10
11
// Concrete Factory for Sea Shipping
public class SeaShippingKitFactory implements ShippingKitFactory {
@Override public Transport createTransport() { return new Ship(); }
@Override public Packaging createPackaging() { return new Container(); }
}
// Concrete Factory for Road Shipping
public class RoadShippingKitFactory implements ShippingKitFactory {
@Override public Transport createTransport() { return new Truck(); }
@Override public Packaging createPackaging() { return new CardboardBox(); }
}
Step 5 — (BONUS) — A Simple Factory to Select the Perfect Kit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ShippingFactoryProvider {
public static ShippingKitFactory getFactory(String shippingType) {
if (shippingType == null) {
throw new IllegalArgumentException("Shipping type cannot be null.");
}
return switch (shippingType.toLowerCase()) {
case "sea" -> new SeaShippingKitFactory();
case "road" -> new RoadShippingKitFactory();
// If you add air shipping type, you only need to add a new case here.
// case "air" -> new AirShippingKitFactory();
default -> throw new IllegalArgumentException("Unsupported shipping type: " + shippingType);
};
}
}
Step 6 — Client and Usage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ShippingManager {
private final Transport transport;
private final Packaging packaging;
public ShippingManager(ShippingKitFactory factory) {
System.out.println("Configuring shipping manager with a new kit...");
this.transport = factory.createTransport();
this.packaging = factory.createPackaging();
}
public void ship(String items, String destination) {
packaging.pack(items);
transport.deliver(destination);
System.out.println("Shipment complete!\n");
}
}
public class Application {
public static void main(String[] args) {
// The decision can come from a config file, user input, etc.
String shippingType = "road";
try {
ShippingKitFactory factory = ShippingFactoryProvider.getFactory(shippingType);
ShippingManager manager = new ShippingManager(factory);
manager.ship("Electronics", "New York Warehouse");
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Spring Boot Context
In a modern Spring Boot application, you wouldn’t typically create a manual ShippingKitFactory. Instead, the Dependency Injection (DI) container manages the creation and wiring of your object families using configuration and profiles.
You define different sets of beans (product families) and activate one set at runtime.
Step 1 — Define Beans for Each Family
You create separate @Configuration classes for each family, associating each with a Spring Profile (@Profile).
Road Shipping Family Configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@Profile("road")
public class RoadShippingConfig {
@Bean
public Transport roadTransport() {
return new Truck();
}
@Bean
public Packaging roadPackaging() {
return new CardboardBox();
}
}
Sea Shipping Family Configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@Profile("sea")
public class SeaShippingConfig {
@Bean
public Transport seaTransport() {
return new Ship();
}
@Bean
public Packaging seaPackaging() {
return new Container();
}
}
Step 2 — Inject the Beans into the Client
The ShippingManager client simply asks for the abstract Transport and Packaging interfaces. Spring’s DI container injects the correct concrete beans based on the active profile.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class ShippingManager {
private final Transport transport;
private final Packaging packaging;
public ShippingManager(Transport transport, Packaging packaging) {
this.transport = transport;
this.packaging = packaging;
}
public void ship(String items, String destination) {
packaging.pack(items);
transport.deliver(destination);
System.out.println("Shipment complete!\n");
}
}
Step 3 — Activate a Profile
You control which “factory” is active by setting the profile in your `application.properties file or via a command-line argument.
In application.properties:
1
spring.profiles.active=road
With this setting, Spring will only create the Truck and CardboardBox beans, ensuring your ShippingManager is configured with a consistent “road” family. Changing the profile to `sea switches the entire family of objects.
6. Advantages & Disadvantages
Advantages
- Ensures Consistency: All products from the same factory are compatible (no Truck + Container mismatches).
- Total Decoupling: Client code depends only on abstract interfaces.
- Swap Families Easily: Replacing behavior across the system is a single factory instantiation away.
Disadvantages
- Hard to Add New Product Types: Adding a new product (e.g.,
Insurance) requires changing theShippingKitFactoryinterface and every concrete factory — breaking Open/Closed for the factories. - More Boilerplate: Extra interfaces and classes can feel heavy for simple cases.
7. When to Use or Avoid
Use it when:
- You need to create families of related objects that must be used together.
- Your system needs multiple consistent configurations/themes (different UI toolkits, database drivers, shipping methods, etc.).
- You want to hide implementation details while guaranteeing compatibility.
Avoid it when:
- The set of product types will change frequently (the factory interface becomes a maintenance burden).
- You only need to create a single product type — prefer the simpler Factory Method or direct constructors.
8. Conclusion
The Abstract Factory pattern is a design powerhouse. It’s the ultimate solution for ensuring that related objects work together as a consistent set. By creating a “factory for factories,” you build robust systems where components are guaranteed to be compatible.
Key Takeaways:
- Core Idea: Create families of related objects.
- Main Benefit: Enforces consistency among products and completely decouples your client from concrete implementations.
- Key Trade-off: Makes it easy to add new families (e.g.,
AirShippingKitFactory) but hard to add new product types (e.g., anInsuranceobject).
Final Thought:
If the Factory Method is about choosing the right tool for the job, the Abstract Factory is about choosing the right, fully-stocked toolbox. It’s a higher level of abstraction, but for complex systems, that level of organization is priceless.
Next Up: Tired of messy constructors with dozens of optional parameters? We’ll explore the Builder Pattern to construct complex objects step by step with clarity and grace.
GitHub Example
You can find the complete, working code example for design patterns in my public GitHub repository. Feel free to clone it and experiment with the code.
- GitHub Repository - Design Patterns: mcakar-dev/design-patterns
- GitHub Repository - Abstract Factory: mcakar-dev/design-patterns - Abstract Factory
References & Further Reading
- Head First Design Patterns by Eric Freeman & Elisabeth Robson
- Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, Vlissides
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin
- Baeldung: Abstract Factory Pattern in Java
- Refactoring.Guru: Abstract Factory
