Loading learning content...
In the previous page, we established the problem: creating families of related objects without scattering conditional logic, coupling to concrete classes, or risking family consistency violations. Now we introduce the Abstract Factory pattern—a solution that elegantly addresses all these concerns.
Core Insight:
Instead of clients creating products directly or through conditionals, introduce a factory interface that declares creation methods for each product type. Provide a concrete factory for each family. Clients code against the factory interface and receive products from whichever concrete factory is injected.
This inversion transforms the problem: instead of knowledge about families being distributed to every creation site, it's concentrated in the concrete factory selection—typically a single configuration decision at application startup.
By the end of this page, you will understand how to design abstract factory interfaces, implement concrete factories for each family, and write client code that creates entire product families through polymorphism. You'll see how this pattern guarantees family consistency by construction.
The foundation of the Abstract Factory pattern is an interface (or abstract class) that declares factory methods for each product type in the family. This interface becomes the contract that all concrete factories must fulfill.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
/** * Abstract Factory interface for creating UI component families. * * Each method declares creation of an abstract product type. * Concrete factories will return family-specific implementations. * * Notice: No family knowledge here. No conditionals. * Just a clean contract for creating a family of products. */public interface GUIFactory { /** * Creates a button appropriate for this factory's family. * @return A Button implementation specific to the family */ Button createButton(); /** * Creates a text field appropriate for this factory's family. * @return A TextField implementation specific to the family */ TextField createTextField(); /** * Creates a menu appropriate for this factory's family. * @return A Menu implementation specific to the family */ Menu createMenu(); /** * Creates a checkbox appropriate for this factory's family. * @return A Checkbox implementation specific to the family */ Checkbox createCheckbox(); /** * Creates a dialog appropriate for this factory's family. * @return A Dialog implementation specific to the family */ Dialog createDialog(); /** * Creates a scroll bar appropriate for this factory's family. * @return A ScrollBar implementation specific to the family */ ScrollBar createScrollBar();}Notice that the interface has no knowledge of families (Windows, macOS, etc.). It doesn't know how many families exist or which one is active. It simply declares what can be created. The abstraction is over the capability to create, not the choice of what to create.
The abstract factory returns abstract products—interfaces or abstract classes that define the contract each product must fulfill. These abstractions shield client code from concrete implementations.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
/** * Abstract product: Button * * Defines the contract for all button implementations. * Concrete buttons (WindowsButton, MacOSButton) must implement this. */public interface Button { void render(); void onClick(Runnable handler); void setEnabled(boolean enabled); void setText(String text); String getText();} /** * Abstract product: TextField * * Defines the contract for all text field implementations. */public interface TextField { void render(); String getValue(); void setValue(String value); void setPlaceholder(String placeholder); void onTextChange(Consumer<String> handler); void setValidation(Predicate<String> validator);} /** * Abstract product: Menu * * Defines the contract for all menu implementations. */public interface Menu { void render(); void addItem(String label, Runnable action); void addSeparator(); Menu addSubmenu(String label); void setEnabled(boolean enabled);} /** * Abstract product: Checkbox * * Defines the contract for all checkbox implementations. */public interface Checkbox { void render(); boolean isChecked(); void setChecked(boolean checked); void setLabel(String label); void onToggle(Consumer<Boolean> handler);} /** * Abstract product: Dialog * * Defines the contract for all dialog implementations. */public interface Dialog { void show(); void hide(); void setTitle(String title); void setContent(Component content); void addButton(Button button); DialogResult getResult();} /** * Abstract product: ScrollBar * * Defines the contract for all scrollbar implementations. */public interface ScrollBar { void render(); void setRange(int min, int max); void setValue(int value); int getValue(); void onScroll(Consumer<Integer> handler);}The Product Interface Design
Notice that the product interfaces define operations that make sense for all implementations, regardless of family. A button can be rendered, clicked, enabled, and labeled—whether it's a Windows button or a macOS button.
Platform-specific capabilities (like macOS button's "pulsing" animation or Windows' "flat style" mode) are either:
Each family has a concrete factory that implements the abstract factory interface. The concrete factory encapsulates all knowledge about creating products for its specific family.
Let's implement factories for Windows and macOS:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
/** * Concrete Factory: Creates Windows-style UI components. * * This factory encapsulates ALL knowledge about Windows UI creation. * Adding Windows-specific configuration happens here, not in client code. */public class WindowsFactory implements GUIFactory { private final WindowsTheme theme; private final DpiScaling dpiScaling; public WindowsFactory() { this.theme = WindowsTheme.detectSystemTheme(); this.dpiScaling = DpiScaling.detectSystemDpi(); } @Override public Button createButton() { WindowsButton button = new WindowsButton(); button.setTheme(theme); button.setDpiScaling(dpiScaling); button.setFlatStyle(theme.usesFlatStyle()); return button; } @Override public TextField createTextField() { WindowsTextField textField = new WindowsTextField(); textField.setTheme(theme); textField.setDpiScaling(dpiScaling); textField.setBorderStyle(WindowsBorderStyle.STANDARD); return textField; } @Override public Menu createMenu() { WindowsMenu menu = new WindowsMenu(); menu.setTheme(theme); menu.setDpiScaling(dpiScaling); menu.setKeyboardNavigationEnabled(true); // Windows convention return menu; } @Override public Checkbox createCheckbox() { WindowsCheckbox checkbox = new WindowsCheckbox(); checkbox.setTheme(theme); checkbox.setDpiScaling(dpiScaling); checkbox.setCheckmarkStyle(WindowsCheckmarkStyle.STANDARD); return checkbox; } @Override public Dialog createDialog() { WindowsDialog dialog = new WindowsDialog(); dialog.setTheme(theme); dialog.setDpiScaling(dpiScaling); dialog.setButtonOrder(ButtonOrder.OK_CANCEL); // Windows convention return dialog; } @Override public ScrollBar createScrollBar() { WindowsScrollBar scrollBar = new WindowsScrollBar(); scrollBar.setTheme(theme); scrollBar.setDpiScaling(dpiScaling); scrollBar.setMinThumbSize(theme.getMinimumScrollThumbSize()); return scrollBar; }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
/** * Concrete Factory: Creates macOS-style UI components. * * This factory encapsulates ALL knowledge about macOS UI creation. * macOS conventions (animations, appearance modes) are configured here. */public class MacOSFactory implements GUIFactory { private final MacOSAppearance appearance; private final RetinaScaling retinaScaling; public MacOSFactory() { this.appearance = MacOSAppearance.detectSystemAppearance(); this.retinaScaling = RetinaScaling.detectDisplayScaling(); } @Override public Button createButton() { MacOSButton button = new MacOSButton(); button.setAppearance(appearance); button.setRetinaScaling(retinaScaling); button.setRoundedCorners(true); // macOS signature style button.setHasPulseAnimation(true); return button; } @Override public TextField createTextField() { MacOSTextField textField = new MacOSTextField(); textField.setAppearance(appearance); textField.setRetinaScaling(retinaScaling); textField.setFocusRingEnabled(true); // macOS focus ring return textField; } @Override public Menu createMenu() { MacOSMenu menu = new MacOSMenu(); menu.setAppearance(appearance); menu.setRetinaScaling(retinaScaling); // macOS: menu bar is at top of screen, not in window menu.setUsesSystemMenuBar(true); return menu; } @Override public Checkbox createCheckbox() { MacOSCheckbox checkbox = new MacOSCheckbox(); checkbox.setAppearance(appearance); checkbox.setRetinaScaling(retinaScaling); checkbox.setAnimated(true); // Smooth check animation return checkbox; } @Override public Dialog createDialog() { MacOSDialog dialog = new MacOSDialog(); dialog.setAppearance(appearance); dialog.setRetinaScaling(retinaScaling); dialog.setButtonOrder(ButtonOrder.CANCEL_OK); // macOS convention: opposite! dialog.setSheetStyle(true); // Slides from title bar return dialog; } @Override public ScrollBar createScrollBar() { MacOSScrollBar scrollBar = new MacOSScrollBar(); scrollBar.setAppearance(appearance); scrollBar.setRetinaScaling(retinaScaling); scrollBar.setOverlayStyle(true); // macOS: scrollbars overlay content scrollBar.setAutoHide(true); return scrollBar; }}Notice how each concrete factory does more than just instantiate objects—it configures them with family-specific settings. Theme detection, DPI scaling, platform conventions (button order!), and animation settings are all handled in one place. Client code doesn't need to know any of this.
Now let's see how client code uses the abstract factory. The key insight: client code depends only on the abstract factory interface and abstract products. It never mentions Windows, macOS, or any concrete class.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
/** * Client code that creates UI using Abstract Factory. * * CRITICAL: This class has NO knowledge of: * - Which platform it's running on * - Which concrete factory is in use * - Which concrete products are being created * * It works exclusively with abstractions. */public class Application { private final GUIFactory factory; // Abstract factory - injected! /** * Constructor injection: the factory is provided from outside. * The choice of which factory (which family) is made elsewhere. */ public Application(GUIFactory factory) { this.factory = factory; } /** * Builds a login form using the injected factory. * This method works identically on Windows, macOS, or Linux. */ public void buildLoginForm() { // Create abstract products - no knowledge of concrete types Button submitButton = factory.createButton(); Button cancelButton = factory.createButton(); TextField usernameField = factory.createTextField(); TextField passwordField = factory.createTextField(); Checkbox rememberMeCheckbox = factory.createCheckbox(); // Configure products using abstract interfaces submitButton.setText("Log In"); cancelButton.setText("Cancel"); usernameField.setPlaceholder("Username"); passwordField.setPlaceholder("Password"); rememberMeCheckbox.setLabel("Remember me"); // Wire up event handlers submitButton.onClick(() -> performLogin( usernameField.getValue(), passwordField.getValue(), rememberMeCheckbox.isChecked() )); cancelButton.onClick(this::closeApplication); // Layout and render - details abstracted renderLoginScreen(submitButton, cancelButton, usernameField, passwordField, rememberMeCheckbox); } /** * Shows an error dialog using the injected factory. */ public void showError(String title, String message) { Dialog errorDialog = factory.createDialog(); Button okButton = factory.createButton(); errorDialog.setTitle(title); okButton.setText("OK"); okButton.onClick(errorDialog::hide); errorDialog.addButton(okButton); errorDialog.show(); } /** * Creates the main application menu. */ public Menu createApplicationMenu() { Menu mainMenu = factory.createMenu(); Menu fileMenu = mainMenu.addSubmenu("File"); fileMenu.addItem("New", this::handleNew); fileMenu.addItem("Open...", this::handleOpen); fileMenu.addItem("Save", this::handleSave); fileMenu.addSeparator(); fileMenu.addItem("Exit", this::closeApplication); Menu editMenu = mainMenu.addSubmenu("Edit"); editMenu.addItem("Undo", this::handleUndo); editMenu.addItem("Redo", this::handleRedo); editMenu.addSeparator(); editMenu.addItem("Cut", this::handleCut); editMenu.addItem("Copy", this::handleCopy); editMenu.addItem("Paste", this::handlePaste); return mainMenu; }}The Application class contains zero platform-specific code. It will work identically with WindowsFactory, MacOSFactory, or any future LinuxFactory. The family selection happens elsewhere—typically at application bootstrap.
The family decision—which concrete factory to use—happens at a single point in the application, typically during bootstrap or configuration. This is where conditional logic rightfully belongs:
The key insight: Conditionals aren't evil. Scattered conditionals are evil. By centralizing the family selection to one location, we get the flexibility of conditional logic without its drawbacks.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
/** * Application bootstrap: the ONLY place where family selection occurs. * * This is the composition root - where we wire dependencies. * All family knowledge is concentrated here. */public class ApplicationBootstrap { /** * Creates the appropriate factory based on the runtime environment. * This is the SINGLE point of family selection in the entire application. */ public static GUIFactory createFactory() { // Detect platform at runtime OperatingSystem os = OperatingSystem.detect(); // Single switch statement - the only place families are mentioned return switch (os) { case WINDOWS -> new WindowsFactory(); case MACOS -> new MacOSFactory(); case LINUX -> new LinuxFactory(); default -> throw new UnsupportedOperationException( "Unsupported operating system: " + os ); }; } /** * Alternative: configuration-driven factory selection. * Useful for testing or forcing a specific look-and-feel. */ public static GUIFactory createFactory(Configuration config) { String factoryType = config.get("ui.factory", "auto"); return switch (factoryType) { case "windows" -> new WindowsFactory(); case "macos" -> new MacOSFactory(); case "linux" -> new LinuxFactory(); case "auto" -> createFactory(); // Fall back to auto-detect default -> throw new ConfigurationException( "Unknown factory type: " + factoryType ); }; } /** * Main entry point: creates and runs the application. */ public static void main(String[] args) { // 1. Create the factory (family selection happens HERE) GUIFactory factory = createFactory(); // 2. Create the application with the factory Application app = new Application(factory); // 3. Run the application - from here on, no family knowledge app.buildLoginForm(); app.createApplicationMenu(); app.run(); // The Application class never knows which factory it received. // It works identically on all platforms. }}The Abstract Factory pattern doesn't just discourage mixing families—it makes mixing structurally impossible in well-designed systems. Let's trace through why:
GUIFactory at construction. All product creation goes through this single factory.factory.createX(). There's no alternative creation path that could bypass the factory.WindowsFactory exists.factory.createButton(), it gets whatever button the factory produces. There's no way to specify "give me a macOS button" because client code doesn't know that macOS buttons exist.The Consistency Trap is Closed
Recall the bug from the problem page:
// This bug is now impossible!
TextField password = new MacOSTextField(); // ← Won't compile!
Why won't it compile? Because the Application class doesn't import MacOSTextField. It only knows about the abstract TextField interface. The only way to get a TextField is through factory.createTextField()—and the factory guarantees family consistency.
For extra protection, concrete product classes can be made package-private (Java) or module-internal (TypeScript/Kotlin), making them genuinely inaccessible outside their package. This transforms "won't import" into "can't import."
One of the pattern's greatest strengths is extensibility. Adding a new family (e.g., Linux UI) requires zero changes to existing client code. Let's trace through what's needed:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
/** * NEW: Concrete Factory for Linux UI components. * * Adding this factory requires: * 1. This new factory class (implements existing interface) * 2. New concrete product classes: LinuxButton, LinuxTextField, etc. * 3. One new case in the bootstrap switch statement * * NO changes to: GUIFactory interface, Application class, * or any other existing code. */public class LinuxFactory implements GUIFactory { private final GTKTheme theme; private final LinuxFontScaling fontScaling; public LinuxFactory() { this.theme = GTKTheme.detectActiveTheme(); this.fontScaling = LinuxFontScaling.detectSystemScaling(); } @Override public Button createButton() { LinuxButton button = new LinuxButton(); button.setTheme(theme); button.setFontScaling(fontScaling); button.setToolkitStyle(detectToolkit()); // GTK, QT, etc. return button; } @Override public TextField createTextField() { LinuxTextField textField = new LinuxTextField(); textField.setTheme(theme); textField.setFontScaling(fontScaling); return textField; } @Override public Menu createMenu() { LinuxMenu menu = new LinuxMenu(); menu.setTheme(theme); menu.setFontScaling(fontScaling); // Linux uses various menu systems based on desktop environment menu.setMenuBackend(detectMenuBackend()); return menu; } @Override public Checkbox createCheckbox() { LinuxCheckbox checkbox = new LinuxCheckbox(); checkbox.setTheme(theme); checkbox.setFontScaling(fontScaling); return checkbox; } @Override public Dialog createDialog() { LinuxDialog dialog = new LinuxDialog(); dialog.setTheme(theme); dialog.setFontScaling(fontScaling); dialog.setButtonOrder(ButtonOrder.CANCEL_OK); // GNOME convention return dialog; } @Override public ScrollBar createScrollBar() { LinuxScrollBar scrollBar = new LinuxScrollBar(); scrollBar.setTheme(theme); scrollBar.setFontScaling(fontScaling); return scrollBar; } private ToolkitType detectToolkit() { // Detect whether running under GTK, QT, or other toolkit return GTKTheme.isGTKEnvironment() ? ToolkitType.GTK : ToolkitType.QT; } private MenuBackend detectMenuBackend() { // Detect AppIndicator, Unity, GNOME Shell, etc. return MenuBackend.detect(); }} // Bootstrap update: ONE new case in existing switchpublic static GUIFactory createFactory() { OperatingSystem os = OperatingSystem.detect(); return switch (os) { case WINDOWS -> new WindowsFactory(); case MACOS -> new MacOSFactory(); case LINUX -> new LinuxFactory(); // ← Only change to existing code! default -> throw new UnsupportedOperationException( "Unsupported operating system: " + os ); };}| Component | Changes Required | Existing Code Modified? |
|---|---|---|
| Abstract Factory Interface | None | ❌ No |
| Abstract Product Interfaces | None | ❌ No |
| Application (Client Code) | None | ❌ No |
| Existing Factories (Windows, macOS) | None | ❌ No |
| New LinuxFactory | Create new class | N/A (new code) |
| New Linux Products | Create new classes | N/A (new code) |
| Bootstrap Switch | Add one case | ⚠️ Minimal |
The Abstract Factory pattern perfectly demonstrates the Open/Closed Principle: the system is open for extension (we added a new family) but closed for modification (no existing code changed except one line in bootstrap). This is the gold standard for extensible design.
We've now seen the complete Abstract Factory solution. Let's consolidate the key points:
What's Next:
In the next page, we'll examine the participants and structure of the Abstract Factory pattern in detail. We'll use UML class diagrams and sequence diagrams to visualize how objects collaborate, and identify the roles and responsibilities of each participant.
You now understand how the Abstract Factory pattern solves the object family problem through interface abstraction and dependency injection. This foundational knowledge prepares you to understand the pattern's formal structure and to recognize it in production codebases.