Back to the path
Level 3 intermediate 26 min #design-patterns#creational-patterns#factory-method#abstract-factory#solid#oop#lld-interview

Factory Method & Abstract Factory: Decoupling Object Creation

Two creational patterns that pull `new` out of client code so you can swap concrete implementations without editing callers. Learn the precise difference between Factory Method (one product, defer to a subclass) and Abstract Factory (a family of related products), build a cross-platform notification-channel example in TypeScript/Python/C++, see exactly where the "consistent family" guarantee is a convention rather than a type-system fact, and learn when these patterns earn their keep versus when they become pure ceremony.

Intent (one line each)

  • Factory Method — Define an interface for creating one object, but let subclasses decide which concrete class to instantiate.
  • Abstract Factory — Provide an interface for creating families of related objects without naming their concrete classes.

Both are creational patterns. Both exist to delete one specific line from your client code: the new ConcreteThing(). Everything else follows from that.

The Problem They Solve

Direct instantiation hard-wires a caller to a concrete class:

class OrderService {
    notify(order) {
        const ch = new EmailChannel();   // <-- the offending line
        ch.send(order.userEmail, "Order shipped");
    }
}

Three things just went wrong:

  1. OrderService now depends on EmailChannel — a concrete, leaf-level class. It can never send an SMS without being edited.
  2. The decision of which channel to use is buried inside business logic, scattered across every call site. Add a PushChannel and you grep for new EmailChannel across the codebase.
  3. It is untestable in isolation — you cannot inject a fake channel; the new is welded in.

The root cause is that selecting a concrete type and using it are two different responsibilities that got fused. Factory patterns split them apart: a creation seam (“which class?”) on one side, polymorphic use (“call send”) on the other.

Key idea

The litmus test for reaching for a factory: the concrete type to instantiate varies (by config, platform, tenant, message type) but the code that uses the object should not care. If the type never varies, you do not need a factory — just call new.

Factory Method vs Abstract Factory — the one-sentence distinction

Factory Method creates one product through a single overridable method. Abstract Factory creates a set of products that must be used together (a family) through an object that bundles several creation methods.

If you only ever vary one product, Factory Method. If varying one choice (say, “macOS theme”) forces a coordinated set of choices (MacButton + MacCheckbox + MacMenu that must match), Abstract Factory.

Structure (ASCII UML)

Factory Method

            +----------------------+              +------------------+
            |   Creator (abstract) |              |    Product       |  <<interface>>
            +----------------------+              +------------------+
            | + someOperation()    |    creates   | + use()          |
            | # createProduct():   |- - - - - - ->+------------------+
            |       Product  {abs} |                      ^
            +----------------------+                      |
                       ^                          +-------+--------+
                       |                          |                |
          +------------+-----------+      +----------------+ +----------------+
          | ConcreteCreatorA       |      | ConcreteProductA| | ConcreteProductB|
          +------------------------+      +----------------+ +----------------+
          | # createProduct():     |--------------^
          |       ConcreteProductA |
          +------------------------+

someOperation() calls the abstract createProduct(); the subclass binds it.

The shape above is the Non-Virtual Interface (NVI) idiom: the public, non-overridable someOperation() invokes the protected, overridable createProduct(). The base class owns the algorithm; subclasses own only the creation step. We use this exact arrangement in the C++ example below — and it is worth naming in an interview, because it shows you know why the factory method is protected rather than public.

Abstract Factory

        +--------------------------+         +------------------+
        | AbstractFactory  {abs}   |         | AbstractProductA |  <<interface>>
        +--------------------------+ creates +------------------+
        | + createButton(): A      |- - - - >|                  |
        | + createCheckbox(): B    |- - +    +------------------+
        +--------------------------+    |     +------------------+
                  ^                     +- - >| AbstractProductB |  <<interface>>
        +---------+----------+               +------------------+
        |                    |
+----------------+  +----------------+
| MacFactory     |  | WinFactory     |
+----------------+  +----------------+
| createButton() |  | createButton() |  --> returns MacButton / WinButton
| createCheckbox()| | createCheckbox()|  --> returns MacCheckbox / WinCheckbox
+----------------+  +----------------+

One factory instance produces one variant's products. The "consistent family"
holds only as long as the client draws BOTH products from the SAME instance.

Participants

RoleFactory MethodAbstract Factory
Product(s)One product interfaceSeveral product interfaces (a family)
Concrete Product(s)The actual classes returnedConcrete classes, grouped per variant
Creator / FactoryClass with an overridable createX() methodObject exposing multiple createX() methods
Concrete Creator / FactorySubclass that picks the concrete productOne per variant (Mac, Windows…) — picks the whole family
ClientCalls someOperation(), never newHolds an AbstractFactory, never new

A useful mental note: Abstract Factory is conventionally described as being implemented with several Factory Methods (the GoF phrasing). Be precise about what that means. A true GoF Factory Method is an overridable method that a subclass of the creator binds. The per-variant createFormatter() / createTransport() you will see in our EmailFactory / SmsFactory below are concrete, non-overridden methods — each variant just hard-returns its own product. That is closer to the Simple Factory idiom living inside each concrete factory. The composition claim is defensible and traditional, but in an interview you should add the nuance: “Abstract Factory’s creation methods are abstract on the factory interface; whether each concrete factory implements them via a true subclass-overridable Factory Method or just a direct new is an implementation detail — most real code does the latter.”

Real-World Example: a Notification Subsystem

Forget the textbook shapes. Here is a recurring backend problem: you must deliver notifications over email, SMS, and push, the channel is chosen at runtime (user preference, message urgency, A/B config), and adding a new channel must not touch existing call sites. That is Factory Method territory — one product (NotificationChannel), type varies.

// ----- Product -----
interface NotificationChannel {
send(to: string, subject: string, body: string): void;
}

class EmailChannel implements NotificationChannel {
send(to: string, subject: string, body: string): void {
  console.log(`EMAIL -> ${to}: ${subject}\n${body}`);
}
}
class SmsChannel implements NotificationChannel {
send(to: string, _subject: string, body: string): void {
  // NOTE: the 160-char GSM limit is enforced HERE, in the channel.
  console.log(`SMS -> ${to}: ${body.slice(0, 160)}`);
}
}

// ----- Creator -----
abstract class Notifier {
// The Factory Method (overridable by subclasses):
protected abstract createChannel(): NotificationChannel;

// NVI: the public algorithm USES the product, unaware of its type:
notify(to: string, subject: string, body: string): void {
  const channel = this.createChannel();
  channel.send(to, subject, body);
}
}

class EmailNotifier extends Notifier {
protected createChannel(): NotificationChannel { return new EmailChannel(); }
}
class SmsNotifier extends Notifier {
protected createChannel(): NotificationChannel { return new SmsChannel(); }
}

// ----- Client -----
const notifier: Notifier = new SmsNotifier();   // chosen by config/DI
notifier.notify("+15550100", "Shipped", "Your order is on its way!");
Tip

Language note worth saying out loud in an interview. In TypeScript and Python the type relationship is structural / duck-typed — implements NotificationChannel and ABC/@abstractmethod are documentation and safety aids (the TS compiler and Python’s ABC will catch a missing method, but at runtime any object with a send would work). In C++ the polymorphism is nominal and explicit: you must have a virtual interface and override, or there is no dynamic dispatch at all. Same pattern, three different enforcement models.

Tip

Static / parameterized factory vs Factory Method. A static NotificationChannel.of(type) that switches on an enum is a Simple Factory (an idiom, not a GoF pattern). It is perfectly fine and often preferable when you do not need subclass-level extension. True Factory Method uses inheritance + an overridable method so that a subclass of the creator, not a switch, supplies the product. A cleaner Simple Factory replaces the switch with a registryMap<Type, () => NotificationChannel> — so adding a channel is a one-line registration instead of editing a switch (which keeps it Open/Closed). In an interview, name which one you mean — they get conflated constantly.

When one choice forces a whole family: Abstract Factory

Now raise the stakes. Each channel needs two collaborating objects that must match: a MessageFormatter (how to lay out the payload) and a Transport (the wire client). An SMS payload must be formatted and transported the SMS way — you must never pair an EmailFormatter with an SmsTransport. The choice of channel selects a consistent family. That is Abstract Factory.

Notice a deliberate responsibility shift from the previous example: in Factory Method the 160-char SMS truncation lived in SmsChannel.send (a transport concern). Here it lives in SmsFormatter.format, and SmsTransport.deliver does no truncation at all. That is intentional — once formatting is its own product, layout/length rules belong to the formatter, not the transport. When you split a god-object into a family, you also get to re-home its responsibilities; call that out rather than letting it drift silently.

interface MessageFormatter { format(subject: string, body: string): string; }
interface Transport { deliver(to: string, payload: string): void; }

// ----- Abstract Factory -----
interface ChannelFactory {
createFormatter(): MessageFormatter;
createTransport(): Transport;
}

// ----- Email family -----
class EmailFormatter implements MessageFormatter {
format(s: string, b: string) { return `Subject: ${s}\n\n${b}`; }
}
class EmailTransport implements Transport {
deliver(to: string, p: string) { console.log(`[smtp] ${to}\n${p}`); }
}
class EmailFactory implements ChannelFactory {
createFormatter() { return new EmailFormatter(); }
createTransport() { return new EmailTransport(); }
}

// ----- SMS family -----
class SmsFormatter implements MessageFormatter {
// 160-char limit now lives in the FORMATTER (layout concern), not transport.
format(_s: string, b: string) { return b.slice(0, 160); }
}
class SmsTransport implements Transport {
deliver(to: string, p: string) { console.log(`[sms-gw] ${to}: ${p}`); }
}
class SmsFactory implements ChannelFactory {
createFormatter() { return new SmsFormatter(); }
createTransport() { return new SmsTransport(); }
}

// ----- Client: depends only on the abstract factory -----
// Consistency is guaranteed ONLY because both products come from the SAME 'f'.
function dispatch(f: ChannelFactory, to: string, subj: string, body: string) {
const payload = f.createFormatter().format(subj, body);  // family member A
f.createTransport().deliver(to, payload);                // family member B (matched via f)
}

dispatch(new SmsFactory(), "+15550100", "Shipped", "Your order is on its way!");
Watch out

The “consistent family” guarantee is a convention, not a type-system fact. Nothing above structurally stops a caller from writing new EmailFormatter() paired with new SmsTransport(), or from passing two different factories into the same flow. The consistency holds only because dispatch draws both products from one factory reference f. The way you enforce it in real code is dependency injection: inject a single ChannelFactory variable at the composition root and forbid new of concrete products in business code. If you need a compile-time guarantee instead of a discipline, you reach past Abstract Factory for sealed/closed product hierarchies, generics tying the products to a variant tag, or a single createBundle() that hands back both products as one matched unit. Stating this limitation out loud is exactly the kind of judgment an SSE interview probes for.

When To Use — and When NOT To

Use Factory Method when:

  • One product type varies and you want subclasses (not a switch) to choose it, often as the variable step of a Template Method.
  • You are writing a framework: the base class calls createX(), and downstream users subclass to supply their type.

Use Abstract Factory when:

  • You have two or more products that must be used together and mismatching them is a bug (formatter+transport, button+checkbox+menu, parser+serializer for one wire format).
  • You switch whole families at once (platform, cloud provider, on-prem vs SaaS driver set).

Do NOT use them when:

  • The concrete type never varies at runtime. If notifications are always email, an EmailNotifier hierarchy is pure ceremony — just call new EmailChannel(). The pattern earns its keep only when the variance is real.
  • There is exactly one family member. An “Abstract Factory” with a single createX() is just a Factory Method wearing a costume; collapse it.
  • A registry/Simple Factory is enough. If you never need a subclass to override creation, a Map<enum, supplier> is simpler than a Creator hierarchy and stays Open/Closed.
  • You are guessing at future variance. Adding the seam “in case we need SMS later” is speculative generality (YAGNI). Add the factory when the second implementation actually arrives.
Common pitfall

The Open/Closed asymmetry SSE interviews love. Adding a new product variant is cheap and Open/Closed for both patterns: Factory Method gets a new Creator subclass; Abstract Factory gets a new concrete factory. But adding a new product method to an Abstract Factory (say createRetryPolicy()) forces you to edit every existing concrete factory — EmailFactory, SmsFactory, PushFactory, all of them. Abstract Factory is Open/Closed along the variant axis and decidedly not along the product-kind axis. If you expect the set of product kinds to churn, Abstract Factory will fight you.

Relationship to SOLID

  • DIP (Dependency Inversion): This is the whole point. The client depends on the NotificationChannel / ChannelFactory abstraction, never on EmailChannel. High-level policy stops depending on low-level detail.
  • OCP (Open/Closed): Adding a new variant is a new class, not an edit to callers — along the variant axis (see the pitfall above for the limit).
  • SRP (Single Responsibility): “Decide which class” is lifted out of “use the object.” The creator owns selection; the client owns behavior.
  • LSP (Liskov): Every concrete product must be substitutable behind the product interface, or the polymorphic notify/dispatch breaks. The pattern relies on LSP holding.

Siblings It Gets Confused With

  • Simple / Static Factory — a method that switches on a parameter. Not a GoF pattern. No subclassing. Great default; reach for the real patterns only when you need the inheritance seam or the family.
  • Builder — also creational, but solves a different problem: constructing one complex object step by step (many optional parts), not selecting among types or families. Factory answers “which class?”; Builder answers “how to assemble this one instance.”
  • Prototype — creates new objects by cloning an existing instance rather than calling a constructor. Use it when instantiation is expensive or the concrete type is only known as a live object.
  • Template Method — Factory Method is frequently the customizable step inside a Template Method (notify() is the template; createChannel() is the hook). They co-occur constantly.
Key idea

Interview compression. “Factory Method = one product, chosen by a subclass via an overridable hook. Abstract Factory = a family of products that must match, chosen by swapping one factory object. Abstract Factory’s consistency is a DI discipline, not a type guarantee. Both are Open/Closed for new variants; Abstract Factory is not Open/Closed for new product kinds. If the type never varies, neither pattern is justified — just call new.”

Assessment

Pass mark: 70% on the concept questions unlocks the next lesson.

1. A service must deliver notifications over exactly one product type — a `NotificationChannel` — chosen at runtime, and you want each variant chosen by a *subclass* of the creator overriding a single hook method that the base class's `notify()` template calls. Which pattern is the most precise fit?

2. Your Abstract Factory exposes `createFormatter()` and `createTransport()`. Product management asks you to add a third product kind, `createRetryPolicy()`, to every channel. What is the cost, and what does it reveal?

3. Which statements about the 'consistent family' guarantee in Abstract Factory are TRUE? (Select all that apply.) (select all)

4. You are reviewing code where notifications are ALWAYS sent by email and there is no foreseeable second channel, yet a `Notifier`/`EmailNotifier` Factory Method hierarchy has been introduced. The best critique is:

Design problem 5

Design a payment-processing subsystem that must support multiple providers (Stripe, Adyen). Each provider requires THREE collaborating objects that must be used together: a `PaymentClient` (charges a card), a `WebhookVerifier` (validates the provider's signed callbacks), and a `RefundClient`. The provider is chosen per-merchant at runtime from config. Identify the correct GoF pattern, sketch the interfaces/classes (one variant fleshed out is enough), and show how a client uses it. Then explain (a) how you guarantee a merchant never mixes a Stripe verifier with an Adyen client, and (b) the Open/Closed cost of later adding a fourth product kind (e.g. a `DisputeClient`) to every provider.