Abstract Factory — Middle Level¶
Source: refactoring.guru/design-patterns/abstract-factory Prerequisite: Junior
Table of Contents¶
- Introduction
- When to Use
- When NOT to Use
- Real-World Cases
- Code Examples — Production-Grade
- Trade-offs
- Alternatives Comparison
- The "Abstract Factory Dilemma"
- Refactoring Toward and Away
- Edge Cases
- Tricky Points
- Best Practices
- Summary
- Diagrams
Introduction¶
Focus: Why and When
Abstract Factory is the right tool when product families exist as a real concept — Windows widgets together, Mac widgets together, Postgres types together, MySQL types together. Use it when mixing variants would be a bug.
The middle-level skill is recognizing the difference between "these things go together" (Abstract Factory) and "this is a configurable variant" (Factory Method, Strategy, or DI). The former implies a family with internal consistency; the latter implies a single decision point.
This file explores production cases, the famous "abstract factory dilemma," and migration paths.
When to Use¶
Use Abstract Factory when all of the following hold:
- Multiple product types must be created together (≥ 2 distinct types).
- Multiple variants of those types exist (≥ 2 variants).
- Family consistency matters — mixing variants would be wrong.
- The set of variants is reasonably stable — adding a new variant is common; adding a new product type is rare.
Strong-fit examples¶
- Cross-platform UI: Button + Checkbox + Window per OS.
- Database access: Connection + Query + Transaction per dialect.
- Cloud SDK: Storage + Queue + Compute per provider.
- Theme engines: Background + Text + Border per theme.
- Cipher suites: Hash + Cipher + KeyExchange per provider.
When NOT to Use¶
| Anti-pattern symptom | Better choice |
|---|---|
| Only one variant ever | Factory Method or direct new |
| Products don't form a real family | Multiple separate Factory Methods |
| Variants change very often, but product types are stable | Strategy per type |
| Family construction is multi-step | Builder |
| You need runtime configuration of which products belong | DI container |
| Set is closed and known | Plain factories or enum-driven dispatch |
Real-World Cases¶
1. Cross-Platform UI¶
abstract class GuiFactory {
abstract Button createButton();
abstract Checkbox createCheckbox();
abstract Slider createSlider();
}
class WindowsGuiFactory extends GuiFactory { /* all Windows-styled */ }
class MacGuiFactory extends GuiFactory { /* all Mac-styled */ }
class WebGuiFactory extends GuiFactory { /* all HTML/CSS-styled */ }
The toolkit's renderer doesn't care which OS is running — it just calls factory.createX().
2. Database Access (JDBC-style)¶
interface DialectFactory {
Connection createConnection(String dsn);
PreparedStatement prepare(Connection c, String sql);
Transaction beginTransaction(Connection c);
}
class PostgresDialect implements DialectFactory { /* PG-specific */ }
class MySQLDialect implements DialectFactory { /* MySQL-specific */ }
class SQLiteDialect implements DialectFactory { /* SQLite-specific */ }
3. Cloud SDK (AWS/GCP/Azure)¶
interface CloudFactory {
BlobStore createStorage(String bucket);
Queue createQueue(String topic);
Compute createCompute(String region);
}
class AwsFactory implements CloudFactory { /* S3 + SQS + EC2 */ }
class GcpFactory implements CloudFactory { /* GCS + PubSub + GCE */ }
class AzureFactory implements CloudFactory { /* Blob + Service Bus + VM */ }
Application code calls the abstract methods; the deployment chooses one factory.
4. Game Asset Themes¶
class LevelTheme(ABC):
@abstractmethod
def make_tree(self) -> Tree: ...
@abstractmethod
def make_rock(self) -> Rock: ...
@abstractmethod
def make_enemy(self) -> Enemy: ...
class ForestTheme(LevelTheme):
def make_tree(self): return PineTree()
def make_rock(self): return MossyRock()
def make_enemy(self): return Wolf()
class DesertTheme(LevelTheme):
def make_tree(self): return Cactus()
def make_rock(self): return SandstoneRock()
def make_enemy(self): return Scorpion()
The level renderer iterates and creates assets by role, not type — keeps Forest objects out of Desert levels.
5. Cipher Suites¶
package crypto
// Abstract product types
type Hasher interface { Sum([]byte) []byte }
type Cipher interface { Encrypt([]byte, []byte) []byte }
// Abstract Factory: returns a matched suite
type Suite interface {
Hasher() Hasher
Cipher() Cipher
}
// Concrete factories: each provides the matching suite
type fipsSuite struct{}
func (fipsSuite) Hasher() Hasher { return fipsSHA256{} }
func (fipsSuite) Cipher() Cipher { return fipsAES{} }
type bouncySuite struct{}
func (bouncySuite) Hasher() Hasher { return bouncySHA256{} }
func (bouncySuite) Cipher() Cipher { return bouncyAES{} }
A FIPS-mode application chooses fipsSuite{}; a non-restricted one chooses bouncySuite{}.
Code Examples — Production-Grade¶
Java — Storage Backend Family¶
public interface StorageFactory {
BlobStore blobStore(String bucket);
KvStore kvStore(String namespace);
StreamLog streamLog(String topic);
}
public class AwsStorageFactory implements StorageFactory {
private final AwsConfig config;
public AwsStorageFactory(AwsConfig config) { this.config = config; }
public BlobStore blobStore(String bucket) { return new S3BlobStore(config, bucket); }
public KvStore kvStore(String namespace) { return new DynamoKvStore(config, namespace); }
public StreamLog streamLog(String topic) { return new KinesisStreamLog(config, topic); }
}
public class LocalStorageFactory implements StorageFactory {
private final Path root;
public LocalStorageFactory(Path root) { this.root = root; }
public BlobStore blobStore(String bucket) { return new FsBlobStore(root.resolve(bucket)); }
public KvStore kvStore(String namespace) { return new SqliteKvStore(root.resolve(namespace + ".db")); }
public StreamLog streamLog(String topic) { return new FileStreamLog(root.resolve(topic + ".log")); }
}
The LocalStorageFactory lets developers run the app entirely on their laptop with no cloud accounts.
Python — Plugin-style Theme System¶
from abc import ABC, abstractmethod
from typing import Type
class Renderer(ABC):
@abstractmethod
def render(self, content: str) -> str: ...
class Header(Renderer): ...
class Body(Renderer): ...
class Footer(Renderer): ...
class ThemeFactory(ABC):
@abstractmethod
def header(self) -> Header: ...
@abstractmethod
def body(self) -> Body: ...
@abstractmethod
def footer(self) -> Footer: ...
# A registry of factories, plugin-style
_THEMES: dict[str, Type[ThemeFactory]] = {}
def register_theme(name: str):
def decorator(cls: Type[ThemeFactory]):
_THEMES[name] = cls
return cls
return decorator
@register_theme("light")
class LightTheme(ThemeFactory):
def header(self): return LightHeader()
def body(self): return LightBody()
def footer(self): return LightFooter()
@register_theme("dark")
class DarkTheme(ThemeFactory):
def header(self): return DarkHeader()
def body(self): return DarkBody()
def footer(self): return DarkFooter()
def render_page(theme: str) -> str:
factory = _THEMES[theme]()
return "\n".join([factory.header().render(""), factory.body().render(""), factory.footer().render("")])
The @register_theme decorator turns adding a new theme into a single-file change.
Go — Cloud SDK Adaptation¶
package cloud
import "context"
// ── Abstract products ──
type Storage interface {
Get(ctx context.Context, key string) ([]byte, error)
Put(ctx context.Context, key string, val []byte) error
}
type Queue interface {
Send(ctx context.Context, msg []byte) error
Receive(ctx context.Context) ([]byte, error)
}
// ── Abstract Factory ──
type Provider interface {
Storage(name string) Storage
Queue(name string) Queue
}
// ── Concrete Factory: AWS ──
type aws struct { cfg AwsConfig }
func (a *aws) Storage(name string) Storage { return &s3Bucket{a.cfg, name} }
func (a *aws) Queue(name string) Queue { return &sqsQueue{a.cfg, name} }
func NewAWS(cfg AwsConfig) Provider { return &aws{cfg} }
// ── Concrete Factory: Local (for tests/dev) ──
type local struct { root string }
func (l *local) Storage(name string) Storage { return &fsBucket{l.root, name} }
func (l *local) Queue(name string) Queue { return &channelQueue{name: name} }
func NewLocal(root string) Provider { return &local{root} }
// Client code
func ProcessOrder(p cloud.Provider, orderID string) error {
bucket := p.Storage("orders")
queue := p.Queue("notifications")
// ... uses bucket and queue without knowing AWS vs local
return nil
}
The same ProcessOrder works against AWS in production and the local file system in tests.
Trade-offs¶
| Dimension | Abstract Factory | Multiple Factory Methods | DI Container |
|---|---|---|---|
| Family consistency | Enforced by structure | Manual (callers must match) | Container-controlled |
| Adding a variant | New Concrete Factory | New Concrete Creator per type | New binding set |
| Adding a type | All factories change | Each Creator hierarchy independent | New binding |
| Compile-time safety | High | High | Low (runtime) |
| Boilerplate | High | Medium | Configuration-heavy |
| Discoverability | Good (one interface) | Scattered | Hidden in container |
Alternatives Comparison¶
vs Factory Method¶
- Factory Method: one product per Creator. Subclass picks the type.
- Abstract Factory: family of products per Concrete Factory. The factory itself is the variant.
If the products you create don't relate to each other, you don't need Abstract Factory — multiple Factory Methods suffice.
vs Builder¶
- Abstract Factory: product immediately constructed, family consistency.
- Builder: product constructed step-by-step, often with optional parts.
A Builder can be used to build products inside Abstract Factory methods, but they answer different questions.
vs Prototype¶
- Abstract Factory: create from scratch.
- Prototype: clone existing.
Combine when construction is expensive and cloning is cheap (Prototype-based Abstract Factory).
vs DI¶
- Abstract Factory: the variant is a class; you instantiate it.
- DI: the variant is a configuration; the container instantiates it.
DI is more flexible for runtime configuration; Abstract Factory is more visible.
The "Abstract Factory Dilemma"¶
The pattern's biggest weakness: the variant axis and the type axis are asymmetric.
| Operation | Cost |
|---|---|
| Add a new variant (Concrete Factory) | One new file. Easy. |
| Add a new product type (factory method) | Modify every Concrete Factory + Abstract Factory. Hard. |
Example:
interface GuiFactory {
Button createButton();
Checkbox createCheckbox();
// Adding `Slider createSlider()` requires updates to:
// - GuiFactory itself
// - WindowsGuiFactory
// - MacGuiFactory
// - WebGuiFactory
// - Any other Concrete Factory
// ...and you must implement Windows, Mac, Web sliders.
}
Your codebase becomes "easy to add OS, hard to add UI element."
Mitigations¶
- Default methods (Java 8+): add
defaultmethods to the interface that throwUnsupportedOperationException. New types don't break old factories. - Composition over Abstract Factory: split into multiple Factory Method hierarchies, accepting that family consistency must be tested.
- Visitor for cross-cutting operations: if operations multiply faster than products, Visitor scales better.
- Code generation: generate Concrete Factories from a spec.
Refactoring Toward and Away¶
Toward — Multiple Factories → Abstract Factory¶
You have:
interface ButtonFactory { Button create(String os); }
interface CheckboxFactory { Checkbox create(String os); }
interface SliderFactory { Slider create(String os); }
…with each one doing if (os.equals("win")) ... else if ("mac") ....
Step 1 — Define Abstract Factory:
Step 2 — One Concrete Factory per OS:
class WindowsGuiFactory implements GuiFactory {
public Button createButton() { return new WindowsButton(); }
public Checkbox createCheckbox() { return new WindowsCheckbox(); }
public Slider createSlider() { return new WindowsSlider(); }
}
Step 3 — Replace all if/else with the factory:
GuiFactory f = pickFactoryForOS();
Button b = f.createButton();
Checkbox c = f.createCheckbox();
Slider s = f.createSlider();
The OS check happens once, in pickFactoryForOS() — not scattered across every product creation.
Away — Abstract Factory → DI¶
When the codebase outgrows the manual factory hierarchy:
@Configuration
class AwsConfig {
@Bean BlobStore blobStore() { return new S3BlobStore(); }
@Bean Queue queue() { return new SqsQueue(); }
}
@Configuration
@Profile("local")
class LocalConfig {
@Bean BlobStore blobStore() { return new FsBlobStore(); }
@Bean Queue queue() { return new InMemoryQueue(); }
}
The DI container is the Abstract Factory. The "family" is now a configuration profile.
Edge Cases¶
1. Adding a new product type breaks all factories¶
Java will give compile errors. Go will too (interface implementations). Python won't catch this until runtime — be vigilant.
2. Inconsistent variants in the family¶
A buggy WindowsFactory returns a MacButton somewhere — same return type, wrong variant. Compiler can't detect.
Test:
@Test
void windowsFactoryProducesAllWindowsVariants() {
GuiFactory f = new WindowsGuiFactory();
assertThat(f.createButton()).isInstanceOf(WindowsButton.class);
assertThat(f.createCheckbox()).isInstanceOf(WindowsCheckbox.class);
}
3. Product needs another product¶
class WindowsCheckbox {
private final WindowsButton parent;
WindowsCheckbox(WindowsButton parent) { this.parent = parent; }
}
The factory must coordinate:
public Checkbox createCheckbox(Button parent) { return new WindowsCheckbox((WindowsButton) parent); }
This couples the factory back to concrete types — design the products to avoid cross-references when possible.
4. Singleton factories¶
Concrete factories are usually stateless — make them singletons. But: testing can leak state. See Singleton — Senior for testability strategies.
Tricky Points¶
- The pattern is "abstract" twice: the factory interface is abstract, and the products it returns are also abstract.
- Adding a fourth product to a 3-product family is the refactoring nightmare. Plan the family axes carefully up front.
- You can't easily mix variants even if you want to — that's a feature, not a bug.
- The factory is often a Singleton because it's stateless and there's only one variant active per process at a time.
- In Go, family consistency relies on convention — a unit test per Concrete Factory verifies the contract.
Best Practices¶
- Pick the family axes carefully. Adding a product type later is expensive.
- Test family consistency. A unit test per Concrete Factory.
- Make factories stateless. Use singletons or short-lived instances.
- Default methods (Java 8+) for new optional types — graceful evolution.
- Document what "family" means — visual style? Performance characteristics? Both?
- One factory per process — pick at startup; don't switch mid-flight unless you have a specific reason.
Summary¶
- Abstract Factory = factory of factories, one method per product type, all returning matching variants.
- Use when product families are real and consistency matters.
- The "abstract factory dilemma": easy to add variants, hard to add types.
- In Go, adapt with interfaces — family consistency is a convention.
- Often paired with Singleton, evolves toward DI for large systems.
Diagrams¶
The Two Axes¶
Storage Provider¶
← Junior · Creational · Roadmap · Next: Senior