Abstract Factory: One Pattern, Many Clean Solutions
In our last article, Tangled Communication Logic? You Don’t Need Smarter Objects — You Need a Mediator, we addressed how the Mediator pattern can bring order to a network of interacting objects. Now we turn our attention to the realm of object creation. If you’ve ever found your codebase cluttered with if/else or switch statements deciding which class to instantiate for a given situation, it might be time to reach for the Abstract Factory pattern. This creational pattern helps manage complexity by grouping related object creation logic into coherent “factories,” making code easier to extend and maintain.
What is the Abstract Factory Pattern?
At its core, the Abstract Factory pattern provides an interface for creating families of related objects without tying your code to specific concrete classes. In other words, it’s a way to encapsulate a group of individual factories (one for each kind of product to create) under a higher-level interface, without exposing the actual classes being instantiated. The client code asks the abstract factory for objects, but only ever sees the abstract interfaces of those objects, never the concrete implementations.
This pattern is especially useful when there are multiple variants of a group of products. For example, consider a system that needs to work with multiple third-party services or multiple deployment platforms. If you have a set of related classes that differ between Platform A and Platform B, an Abstract Factory can provide a clean interface to get the right objects for each platform without littering your code with platform checks. The key is that the objects produced by one factory are designed to work together and are all appropriate for a given context or variant.
How is this different from the Factory Method pattern? The Factory Method is a simpler pattern that uses a single method (often via inheritance) to create one kind of product. Abstract Factory is a higher-level abstraction: instead of a single method, you get an object that offers multiple factory methods, one for each product in a family. Another way to look at it: Factory Method uses class inheritance to decide which object to create (by overriding a factory method in subclasses), whereas Abstract Factory uses object composition — you create a factory object that encapsulates the creation of a variety of products. If you find yourself needing multiple factory methods or switching on a configuration value in various places to create related objects, that’s a sign an Abstract Factory may be more appropriate than scattered factory methods.
A Real-World Analogy
To ground this concept, let’s use a real-world analogy. Imagine you’re building a custom PC. You have a choice between two families of components: one based on Intel and another based on AMD. These two families have parts that are meant to work together: an Intel CPU requires a motherboard with an Intel-compatible socket, whereas an AMD CPU needs an AMD-compatible board. If you mix and match an AMD processor with an Intel motherboard, things won’t work properly — the parts are incompatible.
An Abstract Factory in this scenario would be like a PC parts supplier that sells you a bundle of matching components. If you choose the “Intel kit,” you get an Intel CPU and a compatible Intel motherboard. If you choose the “AMD kit,” you’ll receive an AMD CPU with an AMD-compatible motherboard. Either way, you interact with the supplier through the same general interface (you just ask for a kit), and you trust that the kit’s components will be compatible with each other. You don’t have to worry about the details of socket types or chipsets — the supplier (factory) takes care of that, ensuring that you won’t accidentally mix incompatible objects from different families. In software terms, each kit corresponds to a family of products produced by a concrete factory, and the pattern guarantees you use one family at a time for consistency.
This analogy highlights the main purpose of Abstract Factory: to create sets of objects that belong together. Just as the CPU and motherboard in an Intel kit are designed to work in tandem, the objects generated by a single concrete factory are designed to operate jointly without issues. The client (like our PC builder) doesn’t need to micromanage compatibility of individual parts — it just asks for the appropriate family and gets a suite of compatible objects.
Benefits of Abstract Factory
Using the Abstract Factory pattern can bring several advantages:
Ensures Compatible Products
When you obtain objects from the same factory, you can be confident they are meant to work together smoothly. This consistency is valuable in systems where mixing components from different contexts could cause errors.
Encapsulation of Creation Logic
The pattern separates the what from the how of object creation. Clients don’t need to know the specific classes being created or any initialization intricacies — they interact only with abstract interfaces. This leads to a cleaner, decoupled design where all object instantiation code is centralized in the factory.
Single Responsibility Principle
By extracting object creation code out of business logic and into factories, each part of the codebase has a clearer responsibility. The factory is focused solely on creating products, and the client code focuses on using them. This separation makes the code easier to understand and maintain.
Easy to Swap Families
Since the factory encapsulates a family of products, switching from one family to another is typically a one-line change — you instantiate a different factory. All the resulting objects change together to the new variant, with minimal impact on client code.
Open/Closed Principle for Families
You can introduce new variants or families of products without breaking existing client code, as long as you provide a new factory that implements the same abstract factory interface. For instance, if a new third platform needs support, you can add a new concrete factory for it. The client can start using it by switching which factory it is given, without any changes to the code that uses the products.
Drawbacks and Trade-offs
Like any design pattern, Abstract Factory is not a universal fix. There are some downsides and challenges to consider:
Increased Complexity and Boilerplate
The Abstract Factory pattern introduces additional layers — you will end up writing interfaces and classes for the factories and for the products. In a small or simple project, this may feel like overkill. If you only have a couple of variations or if the creation logic is straightforward, a simple factory function or Factory Method might suffice.
Harder to Add New Product Types
While adding a new family (variant) is straightforward, adding a new kind of product to an existing family requires changes in multiple places. For example, if our PC parts supplier decides to include a GPU in the kits as well, they’d need to update the abstract factory interface (to add a method like createGPU()
), and every concrete factory (IntelFactory, AMDFactory, etc.) must implement it. This touches a lot of code, potentially violating Open/Closed Principle at the factory level. In other words, the pattern makes it easy to add new families, but not as easy to add new product types.
More Classes to Manage
Introduce a few families with a few product types each, and you get a proliferation of classes that can be daunting to navigate. This can make the code harder to read or trace, especially for newcomers. Using clear naming conventions and organizing files by family can help mitigate this, but it’s a point to keep in mind.
Be Mindful of Dependency Inversion
If client code directly depends on a concrete factory class, you lose some flexibility and may inadvertently introduce tight coupling. Ideally, the client should depend only on the abstract factory interface. One way to ensure this is to use a factory provider or dependency injection to supply the appropriate factory, so the client never explicitly references, say, new IntelFactory()
. The pattern works best when the choice of concrete factory is made in one place (for example, application initialization), and everywhere else relies on the abstraction.
Limitations and When Not to Use It
The Abstract Factory is most effective in specific scenarios, but it’s not always justified. You should probably avoid using Abstract Factory when:
Product Families Aren’t Changing (or Don’t Exist)
If your system doesn’t actually have a need for interchangeable families of products (for example, you only ever use one set of classes for all configurations), introducing an abstract factory adds needless indirection and complexity. The pattern would be over-engineering in such a case.
Only One Product Type Varies
If you’re dealing with a single kind of product that has variants, the Factory Method pattern or even a simple factory function is usually sufficient. Abstract Factory makes the most sense when you have multiple types of objects that vary together as a set. If there’s just one varying object, Abstract Factory is unnecessary overhead.
Overhead Outweighs the Benefit
For small applications or scenarios with limited variation, the burden of maintaining multiple factories and interfaces might outweigh the gains. If adding a new class to handle a new variant is straightforward and low-risk, you might not need the abstraction of an extra factory layer.
Simpler Patterns Will Do
Always gauge if simpler creational patterns (or even basic object-oriented principles) cover your needs. Patterns like Factory Method or Builder can often handle object creation in less complex ways. If they meet the requirements, there’s no need to introduce Abstract Factory just for the sake of it.
Needing Mix-and-Match Flexibility
The Abstract Factory pattern assumes that you will use one “family” of objects at a time for a given context. If your use case actually requires mixing objects from different families simultaneously, Abstract Factory can be too restrictive. In such cases, you might need a different design (or treat those components as individual strategies rather than part of a fixed family).
When to Use Abstract Factory
Consider using the Abstract Factory pattern when these conditions hold:
Multiple Families of Products are Required
Your code needs to support different sets of related objects, and you want to ensure that only compatible combinations are used. For example, supporting different operating systems or integrating with multiple vendors, where each has its own set of implementations, is a strong signal for Abstract Factory.
Consistency Across Products Matters
You want to enforce a level of consistency among a group of objects. By getting all the related objects from one factory, you ensure they share a common theme or dependency. This is useful in scenarios like theming (ensuring a suite of UI components all use the same style) or, say, ensuring a data access layer uses matching implementations for connection, command, and transaction objects.
Clean Separation of Creation Code
You wish to encapsulate complex creation logic and keep it out of the main business code. If constructing objects involves significant setup or choosing between multiple implementations, an Abstract Factory centralizes that decision-making. This makes the system easier to modify or extend without touching the higher-level logic.
Planned Evolution of Systems
If you anticipate that new variants (families) of products might be needed in the future, designing with an Abstract Factory can make your code ready for extension. The client code written today can work with a future family as long as that new family adheres to the same abstract factory interface (no changes needed in client logic when a new factory is introduced).
In summary, use Abstract Factory when you have a family of related objects that you need to instantiate in a consistent manner, and you want to keep the creation logic flexible and decoupled from the rest of your codebase.
Abstract Factory vs. Factory Method
These two creational patterns are often confused — both help decouple code from specific class instantiations. But they solve different problems at different levels of abstraction.
Factory Method is about delegating the creation of a single object to a subclass. You define an abstract method like createProduct()
, and subclasses override it to return the appropriate implementation. This is useful when you have some shared behavior but the actual object being used can vary.
Abstract Factory, on the other hand, operates at a higher level. Instead of one method, it defines a set of factory methods, each responsible for creating one product from a related family. Rather than deciding which object to instantiate in a single method, you’re working with a group of related objects — created together — that are meant to be used in the same context.
A Concrete Example: SDK Initialization in Different Environments
Imagine you’re building a cross-platform mobile SDK with two key components:
- An
HttpClient
for communicating with APIs - A
Logger
for debugging and crash reporting
This SDK needs to behave differently depending on the environment:
- In production, logging should be minimal and the HTTP client optimized for performance.
- In debug, you want detailed logs and a client that supports mocking or inspection.
With Factory Method
You could define an abstract SdkInitializer
class with a method like createLogger()
. Subclasses like ProdSdkInitializer
and DebugSdkInitializer
would override this method to provide the appropriate logger.
abstract class SdkInitializer {
abstract fun createLogger(): Logger
fun start() {
val logger = createLogger()
logger.log("SDK started")
}
}
This works fine when you’re customizing a single object — in this case, the logger.
With Abstract Factory
But when multiple components need to vary together — and especially when they must be consistent with each other — Factory Method starts to break down.
Let’s say your Logger
and HttpClient
need to align with the same environment. You don’t want to accidentally mix a debug logger with a production HTTP client. Here, Abstract Factory is a better fit.
Start by defining an interface that groups together the creation of all environment-dependent components:
interface SdkComponentFactory {
fun createLogger(): Logger
fun createHttpClient(): HttpClient
}
Then implement two concrete factories:
class DebugSdkFactory : SdkComponentFactory {
override fun createLogger() = VerboseLogger()
override fun createHttpClient() = MockHttpClient()
}
class ProdSdkFactory : SdkComponentFactory {
override fun createLogger() = SilentLogger()
override fun createHttpClient() = OptimizedHttpClient()
}
The SDK class then receives a factory and uses it to get the right components:
class Sdk(private val factory: SdkComponentFactory) {
fun start() {
val logger = factory.createLogger()
val client = factory.createHttpClient()
logger.log("Starting SDK with ${client.name}")
}
}
Now, switching environments is just a matter of providing a different factory — the rest of the code stays exactly the same.
Why it matters
- Factory Method is best when you’re customizing a single object and want to keep creation logic in subclasses.
- Abstract Factory is for situations where you need to create a set of related objects that must be used together and stay consistent.
This pattern not only centralizes object creation — it enforces compatibility between products. It guarantees that the Logger
and HttpClient
always match the environment, which is something Factory Method alone cannot enforce.
Example: Abstract Factory in Kotlin
One of the main use cases for the Abstract Factory pattern is platform abstraction — particularly relevant in mobile SDK development. Let’s look at a real-world scenario: supporting both Google Play Services and Huawei Mobile Services (HMS) in a Kotlin-based SDK.
Some Huawei devices lack Google services, including Firebase Cloud Messaging (FCM), so we need to provide an alternative — HMS Push Kit. Similarly, methods for accessing device metadata can differ between platforms. We want to expose a common interface for two services:
PushService
: for sending push notificationsDeviceInfoService
: for retrieving model/brand info
Client code should not worry about what platform it’s running on. Instead, we’ll use Abstract Factory to return a consistent family of services appropriate to the environment.
Define the Interfaces:
// Abstract product interfaces
interface PushService {
fun sendPush(message: String)
}
interface DeviceInfoService {
fun getDeviceModel(): String
}
// Abstract factory interface
interface MobileServicesFactory {
fun createPushService(): PushService
fun createDeviceInfoService(): DeviceInfoService
}
Platform-Specific Implementations:
// Google Play Services implementation
class FcmPushService : PushService {
override fun sendPush(message: String) {
println("Google: Sending push via FCM -> $message")
}
}
class GoogleDeviceInfoService : DeviceInfoService {
override fun getDeviceModel(): String = "Google Pixel 8"
}
// Huawei Mobile Services implementation
class HuaweiPushService : PushService {
override fun sendPush(message: String) {
println("Huawei: Sending push via HMS -> $message")
}
}
class HuaweiDeviceInfoService : DeviceInfoService {
override fun getDeviceModel(): String = "Huawei P50 Pro"
}
Factory Implementations:
class GoogleServicesFactory : MobileServicesFactory {
override fun createPushService(): PushService = FcmPushService()
override fun createDeviceInfoService(): DeviceInfoService = GoogleDeviceInfoService()
}
class HuaweiServicesFactory : MobileServicesFactory {
override fun createPushService(): PushService = HuaweiPushService()
override fun createDeviceInfoService(): DeviceInfoService = HuaweiDeviceInfoService()
}
Using the Factories
We’ll demonstrate both factories explicitly to show how the output changes — while the client logic remains untouched.
fun main() {
println("--- Google Services ---")
val googleFactory = GoogleServicesFactory()
val googlePush = googleFactory.createPushService()
val googleDevice = googleFactory.createDeviceInfoService()
googlePush.sendPush("Welcome from Google stack")
println("Device: ${googleDevice.getDeviceModel()}\n")
println("--- Huawei Services ---")
val huaweiFactory = HuaweiServicesFactory()
val huaweiPush = huaweiFactory.createPushService()
val huaweiDevice = huaweiFactory.createDeviceInfoService()
huaweiPush.sendPush("Welcome from Huawei stack")
println("Device: ${huaweiDevice.getDeviceModel()}")
}
Output:
--- Google Services ---
Google: Sending push via FCM -> Welcome from Google stack
Device: Google Pixel 8
--- Huawei Services ---
Huawei: Sending push via HMS -> Welcome from Huawei stack
Device: Huawei P50 Pro
What This Demonstrates
- You can swap entire families of services by injecting a different factory.
- Client logic is completely decoupled from platform-specific details.
- Abstract Factory enables cohesive configuration — the services created are guaranteed to match their runtime environment.
This is exactly the kind of scenario where the pattern proves valuable: the client sees a clean, unified API, while the concrete implementations and platform-specific details stay hidden behind the factory.
Conclusion
The Abstract Factory pattern is designed to handle one specific need: creating families of related objects without hardcoding their concrete classes. It helps structure systems where components vary together — across platforms, environments, or themes — and where consistency between those components is critical. Instead of scattering conditionals throughout the codebase, you define clear factories that encapsulate object creation in one place. This improves maintainability and reduces the chance of mismatches.
Use Abstract Factory when multiple components in your system need to be coordinated as a group, and when the number of configurations may grow over time. It’s especially relevant in cross-platform SDKs, multi-tenant applications, or anywhere you want to avoid duplication between platform-specific implementations. If you’re only dealing with one object or one variation, Factory Method might be a better fit.
Found the article helpful? Give it a clap, share it with teammates, and follow the blog to get updates on future posts. And feel free to leave a comment — only by learning and sharing experience do we grow.