GUIDE · Creational
Abstract Factory
Provide an interface for creating FAMILIES of related or dependent objects without specifying their concrete classes.
At a glance
- INTENT
- Provide an interface for creating FAMILIES of related or dependent objects without specifying their concrete classes.
- PROBLEM
- Several products must be used together (Stripe charge + Stripe refund + Stripe webhook verifier); mixing families (Stripe charge + PayPal refund) would be a bug the type system can't catch.
- SAY
- Participants: AbstractFactory (createX()/createY()), ConcreteFactory (one per family), AbstractProduct, ConcreteProduct, Client (only sees abstract types). Pitch: "one factory object hands you a whole consistent kit; swap the factory, swap the kit."
- SEEN IN
- Knex dialect Clients (each creates matching QueryCompiler/SchemaCompiler/ColumnCompiler); payment/cloud provider kits (one
providerobject yields charge+refund+verify, or bucket+queue+secrets); @testing-library per-framework packages (render/screen/fireEvent family); cross-platform UI kits (React Native Paper: platform-consistent Button/Input/Dialog set). - VS
- Factory Method = ONE product, chosen by a subclass override. Abstract Factory = a FAMILY, chosen by which factory object you hold (usually implemented WITH factory methods).
The idea#
Picture two kids who cannot share food. One lunch is a pizza kit: pizza crust, pizza sauce, pizza cheese. The other is a taco kit: taco shell, salsa, taco cheese. If you put pizza sauce on a taco shell, you didn't pack two lunches. You packed a mess.
An abstract factory is the person who hands you the whole kit. You don't pick the sauce yourself. You say "pizza kit" or "taco kit", and every piece you get belongs together.
In code the kit is a family of objects that only work as a set. A Stripe charger, a Stripe refunder, and a Stripe signature checker. Or the PayPal versions of those three. The factory is one object with methods like createCharger(), createRefunder(), and createVerifier(). You hold one factory. Everything you create comes from that one family.
Why it matters#
The bug this pattern prevents is mixing families. A Stripe charge id looks like ch_1999. A PayPal refund looks for an id that starts with PAYID-. Charge with Stripe and refund with PayPal, and the refund quietly does the wrong thing. Plain JavaScript will not catch that for you. The factory does, because the code that checks out never reaches into two kits at once.
You already use this without the textbook name. A database client doesn't hand you a Postgres query compiler and a MySQL schema compiler. One client builds the whole set for one database. Swap the client, swap the kit. A cross-platform UI kit works the same way: one factory, and the button, the input, and the dialog all look like they belong on the same phone.
Factory Method is the smaller cousin. It makes one product, and a subclass decides which one. Abstract Factory makes the lunchbox: several products, and the factory object you are holding decides the family. A factory usually does that with factory methods. They stack. They are not rivals.
A small example#
function pizzaKit() { return { crust: () => 'pizza-crust', sauce: () => 'pizza-sauce', };} function tacoKit() { return { crust: () => 'taco-shell', sauce: () => 'salsa', };} function makeLunch(kit) { return { base: kit.crust(), topping: kit.sauce() };} const lunch = makeLunch(pizzaKit());makeLunch only ever sees one kit. You never write pizzaKit().crust() next to tacoKit().sauce().
What the code below is doing#
The sheet shows the same idea twice.
The first shape uses classes so the book names are visible. PaymentFactory is the abstract factory. Its methods throw, because the base kit doesn't know how to make a real product. StripeFactory and PayPalFactory are the concrete factories. StripeCharger, StripeRefunder, and StripeVerifier (and the PayPal twins) are the products. checkoutFlow is the client. It asks the factory for a charger and a refunder, charges, then refunds with the matching refunder. The asserts show a Stripe charge refunds on Stripe and does not refund on PayPal. Calling createCharger on the bare PaymentFactory throws.
The second shape is how you would write it in JavaScript on a normal day: a plain object per family, chosen once from config. providers.stripe and providers.paypal each carry a charger, a refunder, and a verifier. Same rule. Less ceremony.
When you talk about it, say this: one factory object hands you a whole consistent kit. Swap the factory, swap the kit.
Everything below is the demonstration. Same idea, written as code you can run.
'use strict';const assert = require('node:assert/strict');Classic shape (classes, so the participant names are visible)#
// AbstractProducts (interfaces, documented by method shape):// Charger { charge(cents) -> { provider, id } } Refunder { refund(chargeId) -> { refunded } }// Verifier { verify(signature) -> boolean } // ConcreteProducts — Stripe familyclass StripeCharger { charge(c) { return { provider: 'stripe', id: `ch_${c}` }; } }class StripeRefunder { refund(id) { return { refunded: id.startsWith('ch_') }; } }class StripeVerifier { verify(sig) { return sig.startsWith('t='); } }// ConcreteProducts — PayPal familyclass PayPalCharger { charge(c) { return { provider: 'paypal', id: `PAYID-${c}` }; } }class PayPalRefunder { refund(id) { return { refunded: id.startsWith('PAYID-') }; } }class PayPalVerifier { verify(sig) { return sig.length === 8; } } class PaymentFactory { // AbstractFactory createCharger() { throw new Error('abstract'); } createRefunder() { throw new Error('abstract'); } createVerifier() { throw new Error('abstract'); }}class StripeFactory extends PaymentFactory { // ConcreteFactory createCharger() { return new StripeCharger(); } createRefunder() { return new StripeRefunder(); } createVerifier() { return new StripeVerifier(); }}class PayPalFactory extends PaymentFactory { // ConcreteFactory createCharger() { return new PayPalCharger(); } createRefunder() { return new PayPalRefunder(); } createVerifier() { return new PayPalVerifier(); }} // Client — depends only on the abstract factory & productsfunction checkoutFlow(factory, cents) { const charger = factory.createCharger(); const refunder = factory.createRefunder(); const { id, provider } = charger.charge(cents); const { refunded } = refunder.refund(id); // works because both come from one family return { provider, id, refunded };} const s = checkoutFlow(new StripeFactory(), 1999);const p = checkoutFlow(new PayPalFactory(), 1999); // Mixing families is exactly the bug the pattern prevents:const stripeChargeId = new StripeFactory().createCharger().charge(1).id;Verified by 6 asserts
assert.deepEqual(s, { provider: 'stripe', id: 'ch_1999', refunded: true });assert.deepEqual(p, { provider: 'paypal', id: 'PAYID-1999', refunded: true });assert.equal(new PayPalFactory().createRefunder().refund(stripeChargeId).refunded, false);assert.equal(new StripeFactory().createVerifier().verify('t=123,v1=abc'), true);assert.equal(new PayPalFactory().createVerifier().verify('t=123,v1=abc'), false);assert.throws(() => new PaymentFactory().createCharger(), /abstract/);Idiomatic JS shape: a plain object per family, selected once by config#
const providers = { stripe: { charger: () => ({ charge: (c) => ({ provider: 'stripe', id: `ch_${c}` }) }), refunder: () => ({ refund: (id) => ({ refunded: id.startsWith('ch_') }) }), verifier: () => ({ verify: (sig) => sig.startsWith('t=') }), }, paypal: { charger: () => ({ charge: (c) => ({ provider: 'paypal', id: `PAYID-${c}` }) }), refunder: () => ({ refund: (id) => ({ refunded: id.startsWith('PAYID-') }) }), verifier: () => ({ verify: (sig) => sig.length === 8 }), },};const PAYMENT_PROVIDER = 'paypal'; // would come from env/configconst kit = providers[PAYMENT_PROVIDER]; // the "factory" is just the selected object const charge = kit.charger().charge(500);Verified by 4 asserts
assert.equal(charge.provider, 'paypal');assert.equal(kit.refunder().refund(charge.id).refunded, true);assert.equal(kit.verifier().verify('12345678'), true);assert.deepEqual(Object.keys(providers.stripe), Object.keys(providers.paypal)); // same kit shape per familySee also