Skip to content

Deep dive into abstract factory using JavaScript

The first question am sure you are asking yourself is we already have dealt with factory pattern so do they relate with the abstract factory pattern? Yes somehow.

  • Factory says "give me one tool" while the abstract factory says "give me the entire toolbox, but make sure all the tools belong to the same set."
  • Factory method (useful if mixing products wouldn’t break anything) returns one product at a time while the abstract factory (useful mixing products would break consistency) returns a families of related products.
  • Factory pattern creates objects while abstract factory pattern enforces consistency across families of objects.

1. Abstract factory

An Abstract Factory provides an interface for creating families of related objects without specifying their concrete classes. Here we will be using a UI component example to demonstrate abstract factory where we define two families of UI components: LightTheme (LightButton, LightInput) and DarkTheme (DarkButton, DarkInput) note how we pair Light.. and Dark.. to get a consistent UI, check the code below.

js
// Products (Concrete implementations) 
// These are the actual UI components we want to use.
// Notice they belong to two "families": Light and Dark.

class LightButton { 
  render() { console.log("Light Button"); } 
}

class LightInput  { 
  render() { console.log("Light Input");  } 
}

class DarkButton  { 
  render() { console.log("Dark Button");  } 
}

class DarkInput   { 
  render() { console.log("Dark Input");   } 
}


// Abstract Factory 
// This defines the "shape" of a UI factory. It ensures that every 
// concrete factory creates a consistent family of products
// (a button and an input). Clients depend on this abstraction,
//  not the concrete details.

class UIFactory {
  createButton() { throw new Error("override"); }
  createInput()  { throw new Error("override"); }
}


// --- Concrete Factories ---
// Each factory implements UIFactory and returns components from its own
// family. This guarantees consistency. If you're in LightThemeFactory, 
// you will always get light components. If you're in DarkThemeFactory, 
// you'll *always* get dark components.

class LightThemeFactory extends UIFactory {
  createButton() { return new LightButton(); }
  createInput()  { return new LightInput(); }
}

class DarkThemeFactory extends UIFactory {
  createButton() { return new DarkButton(); }
  createInput()  { return new DarkInput(); }
}


// The client doesn't care whether it's dealing with Light or Dark components.
// It just calls the factory. This is the key abstraction benefit:
// swapping out entire families of objects without touching client code.

function renderUI(factory) {
  const b = factory.createButton();
  const i = factory.createInput();
  b.render(); 
  i.render();
}


// Bootstrapping (Dependency Injection)
// Here we decide which factory to inject into the client at runtime.
// This decision can come from an env variable, config file, user settings, etc.
// Dependency Injection = the client never "new"s up concrete classes itself. 
// Instead, it just receives a factory.

const factory = process.env.THEME === "dark" 
  ? new DarkThemeFactory() 
  : new LightThemeFactory();

renderUI(factory);

2. But wait... practically how does abstract factory differ from factory.

Factory Method produces one product at a time here (circle or square per call), while the Abstract Factory produces families of related products.

js
// Factory Method — single product
function shapeFactory(type) {
  if (type === "circle") return { draw: () => "circle" };
  if (type === "square") return { draw: () => "square" };
}

// Abstract Factory — product family
class UIFactory {
  createButton() { throw new Error("override"); }
  createInput() { throw new Error("override"); }
}

3. Abstract factories strength #1- runtime strategy (swapping Aws and Gcp without rewriting logic).

A runtime strategy selector means we don’t hard-code which option to use — instead, we pick the strategy while the program is running, based on context (user choice, environment, config, etc.). In the example below we use abstract factories as a runtime strategy selector so instead of sprinkling if (provider === "aws") … else if (provider === "gcp") … checks throughout our codebase, we centralize the choice in a single factory. Each provider factory (AwsProviderFactory, GcpProviderFactory) produces a consistent family of components — storage + queue — that are guaranteed to match. The bootstrap function then becomes a clean runtime switch: pass "aws" and you get S3 + SQS, pass "gcp" and you get GCS + PubSub.

js
class AwsProviderFactory {
  createStorage() { return { type: "S3" }; }
  createQueue()   { return { type: "SQS" }; }
}

class GcpProviderFactory {
  createStorage() { return { type: "GCS" }; }
  createQueue()   { return { type: "PubSub" }; }
}

function bootstrap(provider) {
  const factory = provider === "aws"
    ? new AwsProviderFactory()
    : new GcpProviderFactory();

  return {
    storage: factory.createStorage(),
    queue: factory.createQueue()
  };
}

console.log(bootstrap("aws"));
// { storage: {type:"S3"}, queue:{type:"SQS"} }

As strategies abstract factories give system-wide coherence.

4. Abstract factories strength #2. Consistent object families.

Abstract Factory guarantees related objects are consistent.

js
// BAD: manual mix → inconsistent family
const btn = new LightButton();
const input = new DarkInput();

// GOOD: factory enforces consistency
const factory = new LightThemeFactory();
const consistentBtn = factory.createButton();
const consistentInput = factory.createInput();

5. Abstract factories strength #3. Easy to switch providers.

By changing one we can change the entire system as shown in the multicloud example below.

js
// Switch between providers
const provider = process.env.PROVIDER === "gcp"
  ? new GcpProviderFactory()
  : new AwsProviderFactory();

const storage = provider.createStorage();
const queue   = provider.createQueue();

6. Abstract factories strength #4. Clean Dependency Injection.

Services are clean and testable when we inject factories instead of putting conditionals all over the code.

js
class FileService {
  constructor(factory) {
    this.storage = factory.createStorage();
  }
}

class QueueService {
  constructor(factory) {
    this.queue = factory.createQueue();
  }
}

const factory = new AwsProviderFactory();
const fileService = new FileService(factory);
const queueService = new QueueService(factory);

7. Abstract factories strength #5. Easy debugging

Factories prevent mismatches that lead to hidden bugs.

js
// Mismatch -  subtle bug
const s3 = new AwsProviderFactory().createStorage();
const pubsub = new GcpProviderFactory().createQueue();
// S3 is eventually consistent, PubSub is strongly consistent - race conditions

// Consistent
const awsFactory = new AwsProviderFactory();
const consistentStorage = awsFactory.createStorage();
const consistentQueue = awsFactory.createQueue();

8. Abstract factories strength #6 Lazy & Cached Product Creation

js
class AwsFactory {
  createS3() {
    if (!this._s3) this._s3 = { client: "S3", init: Date.now() };
    return this._s3; // cached
  }
}

const f = new AwsFactory();
console.log(f.createS3() === f.createS3()); // true

The above AwsFactory example demonstrates how an abstract factory can act as both a creator and a cache, ensuring consistency across object families. Instead of returning a fresh S3 client on each call, it lazily initializes and memoizes the instance, guaranteeing that repeated requests yield the same object (true when compared). This captures two important insights: (1) factories don’t just “create”—they can also enforce lifecycle control and prevent subtle mismatches across your system, and (2) by centralizing instantiation in one place, you reduce the risk of duplicate or conflicting resources (e.g., multiple S3 clients fighting for configuration). It’s a subtle but powerful way abstract factories ensure consistency, efficiency, and cohesion when dealing with infrastructure-level dependencies. Note: this is blending Abstract Factory with a Singleton-style lifecycle it is useful in practice, but conceptually different.

9. Abstract factories strength #7 Composed / Delegating Factories

We can combine factories

js
class CompositeFactory {
  constructor(core, plugin) {
    this.core = core;
    this.plugin = plugin;
  }
  createButton() {
    return this.plugin.createButton?.() || this.core.createButton();
  }
}

In the code above CompositeFactory is a neat application of the Abstract Factory + Strategy hybrid idea: instead of locking us into one factory, it lets us combine a core factory (the default set of UI components) with a plugin factory (an optional override). When createButton() is called, the factory first checks if the plugin knows how to create a button (this.plugin.createButton?.() uses optional chaining). If so, it delegates to the plugin, otherwise it falls back to the core factory’s implementation. This pattern ensures consistent object families (all buttons come from one of the two factories), but it also enables runtime extensibility—plugins can partially override behavior without rewriting everything. The insight here is that this is not just delegation, but a clean way to allow incremental overrides while still guaranteeing that defaults exist, which prevents the subtle mismatched-component bugs that can happen if every override had to be written from scratch.

10. Abstract factories strength #8 Hot reload

Runtime Switching / Hot Reload

js
function hotSwapFactory(initial) {
  let current = initial;
  return {
    setFactory(f) { current = f; },
    createButton() { return current.createButton(); }
  };
}

const proxy = hotSwapFactory(new LightThemeFactory());
console.log(proxy.createButton().render());
proxy.setFactory(new DarkThemeFactory());
console.log(proxy.createButton().render());

In the above code; instead of committing to one family of objects at startup, we wrap the factory in a proxy (hotSwapFactory). The proxy holds a reference to the "current" factory and forwards all creation calls to it. By calling setFactory, we can swap out the entire object family (e.g. Light theme - Dark theme) at runtime without touching the rest of our code. The key insight: this pattern combines Abstract Factory with a Strategy-like runtime switch, letting us change entire ecosystems of components on the fly, while keeping your consumers completely decoupled from the implementation details.

11. Abstract factories strength #9. Contract Validation in Dynamic Javascript

We can create a simple runtime guard to ensure that any factory we pass actually implements the methods we expect (like createButton and createInput). It loops over the required method names and checks if the factory has them as functions; if one is missing, it immediately throws an error. This is especially useful in JavaScript (where there are no interfaces like in TypeScript or Java) because it enforces the Abstract Factory contract at runtime, saving us from subtle bugs when switching factories or plugging in a new one that forgot to implement part of the expected API. This guard can be wrapped in a test helper

js
function assertFactory(factory, methods) {
  methods.forEach(m => {
    if (typeof factory[m] !== "function") {
      throw new Error(`Factory missing method: ${m}`);
    }
  });
}

assertFactory(new LightThemeFactory(), ["createButton","createInput"]);

12. Abstract factories strength #10. Testing becomes easier

In the code below the TestFactory is acting as a mock Abstract Factory: instead of producing real UI components, it returns lightweight test doubles (test-button, test-input). This makes our tests faster, deterministic, and isolated from complex rendering logic rather than pulling in a real DOM button (with styling, events, browser quirks), our unit tests just assert that "test-button" is produced when a button is created. That’s the core advantage — Abstract Factories make it trivial to swap real providers with fake ones, which is exactly why testing becomes easier and safer.

js
class TestFactory {
  createButton() { return { render: () => "test-button" }; }
  createInput()  { return { render: () => "test-input" }; }
}

const testFactory = new TestFactory();
console.log(testFactory.createButton().render()); // test-button

13. What to avoid with abstract factories

js
// Global mutable factory
globalThis.factory = new LightThemeFactory();
globalThis.factory = new DarkThemeFactory(); // implicit switch, hard to debug

// Explicit injection
function render(factory) {
  factory.createButton().render();
}

The above example highlights one of the advantages of treating factories as strategies: explicit dependency injection beats hidden global state every time. In the global version, the active factory is reassigned on globalThis, which makes switching themes implicit, brittle, and prone to race conditions — you never know which part of the app last mutated the global factory. In contrast, the injected version makes the dependency explicit: render(factory) receives its factory as a parameter, so the choice of theme (or provider, or strategy) is visible at the call site and testable in isolation. This is a direct application of the Abstract Factory’s strength — it ensures consistency of object families, but when combined with dependency injection, it also gives you runtime flexibility without hidden coupling or subtle bugs.

when not to use abstract factories

js
// If your app only ever needs one object, e.g.:
function createLogger() {
  return { log: msg => console.log(msg) };
}

At its core, Abstract Factory is not just object creation — it’s system coherence. Treat factories as strategies, and you’ll avoid subtle mismatches, enable runtime flexibility, and keep your architecture clean.

javascript book

If this interested you, check out my Javascript Book

Enjoy every byte.