Creational Pattern Pattern Deep Dive

Abstract Factory

Factory Method solves "which single object should I create." But some situations need more: a whole set of objects that all have to agree with each other. The Abstract Factory pattern answers that broader question: how do you create families of related objects, guaranteeing the pieces you get back are always from the same consistent family, without the client code ever naming a concrete class?

⚠️ The Problem

Imagine a UI toolkit that supports a Light theme and a Dark theme. Every theme needs a matching Button and Checkbox — a light button should never end up paired with a dark checkbox, because the colors, borders, and hover states would clash and look broken. If widget-creation code just does new LightButton() here and new DarkCheckbox() there based on scattered, independent conditionals, nothing stops the two calls from disagreeing about which theme is active. The bug that results isn't a crash — it's a screen that's half light-themed and half dark-themed, and it can slip in anywhere a developer forgets to check the theme flag before creating a widget.

✅ The Solution

Define an abstract factory interface with one creation method per product type in the family — createButton(), createCheckbox(). Each concrete factory (LightThemeFactory, DarkThemeFactory) implements all of those methods together, so grabbing one concrete factory guarantees every product it hands out belongs to the same family. Client code holds a reference to the abstract factory type, asks it for whatever pieces it needs, and never has the chance to accidentally mix a light button with a dark checkbox — the factory itself enforces that the pieces always match.

Example 1 — a UI theme factory

Primary Example — JavaScript
// Abstract products
class Button {
  render() { throw new Error("render() must be implemented"); }
}
class Checkbox {
  render() { throw new Error("render() must be implemented"); }
}

// Concrete products — Light family
class LightButton extends Button {
  render() { console.log("[Light button] white bg, dark text"); }
}
class LightCheckbox extends Checkbox {
  render() { console.log("[Light checkbox] white bg, dark check"); }
}

// Concrete products — Dark family
class DarkButton extends Button {
  render() { console.log("[Dark button] charcoal bg, light text"); }
}
class DarkCheckbox extends Checkbox {
  render() { console.log("[Dark checkbox] charcoal bg, light check"); }
}

// Abstract factory — one method per product type
class UIThemeFactory {
  createButton() { throw new Error("createButton() must be implemented"); }
  createCheckbox() { throw new Error("createCheckbox() must be implemented"); }
}

class LightThemeFactory extends UIThemeFactory {
  createButton() { return new LightButton(); }
  createCheckbox() { return new LightCheckbox(); }
}

class DarkThemeFactory extends UIThemeFactory {
  createButton() { return new DarkButton(); }
  createCheckbox() { return new DarkCheckbox(); }
}

// Client — only ever talks to the abstract factory
function renderSettingsPanel(factory) {
  const button = factory.createButton();
  const checkbox = factory.createCheckbox();
  button.render();
  checkbox.render();
}

renderSettingsPanel(new DarkThemeFactory()); // every widget matches — guaranteed

renderSettingsPanel() never mentions LightButton or DarkCheckbox by name. Whichever factory it's handed, every widget it produces during that call belongs to the same theme — there's no code path where a light button and a dark checkbox both get created for the same panel.

Example 2 — a secondary context: cross-platform native dialogs

Secondary Example — C#
// Abstract products
public interface IDialog {
    void Show(string message);
}
public interface INativeButton {
    void Click();
}

// Windows family
public class WindowsDialog : IDialog {
    public void Show(string message) => Console.WriteLine($"[Win32 dialog] {message}");
}
public class WindowsButton : INativeButton {
    public void Click() => Console.WriteLine("Win32 button click sound");
}

// Mac family
public class MacDialog : IDialog {
    public void Show(string message) => Console.WriteLine($"[Cocoa dialog] {message}");
}
public class MacButton : INativeButton {
    public void Click() => Console.WriteLine("Cocoa button click sound");
}

// Abstract factory
public interface IUIFactory {
    IDialog CreateDialog();
    INativeButton CreateButton();
}

public class WindowsUIFactory : IUIFactory {
    public IDialog CreateDialog() => new WindowsDialog();
    public INativeButton CreateButton() => new WindowsButton();
}

public class MacUIFactory : IUIFactory {
    public IDialog CreateDialog() => new MacDialog();
    public INativeButton CreateButton() => new MacButton();
}

public class SettingsWindow {
    public static void Open(IUIFactory factory) {
        var dialog = factory.CreateDialog();
        var button = factory.CreateButton();
        dialog.Show("Preferences saved");
        button.Click();
    }
}

// SettingsWindow.Open(new MacUIFactory());  // dialog + button both render natively as Mac

Same guarantee, different domain: SettingsWindow.Open() depends only on IUIFactory. Pick WindowsUIFactory and every native control it produces looks and sounds like Windows; pick MacUIFactory and everything matches macOS — the two families can never get cross-wired.

👍 When To Use It

  • Your system needs to work with multiple families of related products (themes, platforms, database drivers) and products from one family must never be mixed with another.
  • You want to enforce, at the type level, that a set of related objects is always created together and stays consistent — not rely on developers remembering to check a flag every time.
  • You expect to add whole new families later (a "High Contrast" theme, a Linux UI factory) without changing the code that consumes the abstract factory.

👎 When NOT To Use It

  • When there's only one family of products and no realistic prospect of a second — the extra abstract factory/product interfaces are pure ceremony with no payoff.
  • When the "family" only has one member (a single product type) — that's just Factory Method; Abstract Factory earns its keep specifically when multiple product types must travel together.
  • When adding a new product type (not a new family) is common — every concrete factory has to grow a new method, which means touching every existing factory implementation, a real maintenance cost of this pattern.

❓ FAQ

How is this different from Factory Method?

Factory Method is a single method, usually overridden through inheritance, that produces one product type. Abstract Factory is an interface with multiple creation methods — one per product type — implemented together by composition, so that a whole family of related objects (a button and a checkbox, a dialog and a button) comes back consistent. In fact, each individual method inside an Abstract Factory (createButton()) is often implemented as its own little Factory Method — the two patterns compose rather than compete.

Do I always need a base "abstract factory" class or interface?

In statically typed languages it's typical (an interface or abstract class), but in dynamically typed languages like JavaScript or Python it's common to skip the formal interface and just pass around objects/functions that satisfy the expected shape — the discipline of "one factory produces a consistent family" is what matters, not a specific keyword.

What happens when I need to add a new product type, like a Slider, to every theme?

Every concrete factory (LightThemeFactory, DarkThemeFactory, and any others) needs a new createSlider() method added. This is the pattern's known weak spot: it's easy to add a new family (a new factory implementing the existing methods) but expensive to add a new product type across all families at once.

🙋 There Are No Dumb Questions

Q: Couldn't I just pass a "theme" string around and branch on it wherever I create a widget?
A: You could, but then the "these must match" rule lives only in the discipline of whoever writes each call site — exactly the bug described in the Problem section above. Abstract Factory moves that rule into the type system: once you've picked a concrete factory, it is structurally impossible to get a mismatched product out of it, because that factory only knows how to build its own family.

✅ Quick Check

What does Abstract Factory guarantee that calling separate constructors for each widget does not?


Next up: what if a class's constructor has grown so many optional parameters that calling it correctly has become a guessing game? That's the problem the Builder pattern solves.

← Back to
Next →