Intent

Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to its subclasses — so the base class can ship a complete workflow without knowing exactly what it's building.

The Problem

Imagine a NotificationService that sends a templated message: render the body, look up the user's preferences, dispatch, log the result. The render and log steps are identical for every channel. The dispatch step is not.

PHP
public function send(User $user, string $message): void
{
    $body = $this->render($message, $user);

    if ($user->prefers === 'email') {
        $this->mailer->send($user->email, $body);
    } elseif ($user->prefers === 'sms') {
        $this->twilio->text($user->phone, $body);
    } elseif ($user->prefers === 'push') {
        $this->fcm->push($user->deviceToken, $body);
    }

    $this->log($user, $message);
}

Every new channel means another branch and another constructor argument and another set of injected dependencies. The whole class drags around the union of every channel's requirements, and you can't ship a half-finished channel without breaking everyone else's tests.

The Solution

Factory Method says: pull the creation of the channel into a method on the base class, and let subclasses override it. The workflow (renderdispatchlog) lives in the base class. The "which channel object do we use?" decision moves into a small overrideable factory method.

PHP
abstract class NotificationService
{
    public function send(User $user, string $message): void
    {
        $body = $this->render($message, $user);
        $this->createChannel()->dispatch($user, $body);  // <- factory method
        $this->log($user, $message);
    }

    abstract protected function createChannel(): NotificationChannel;
}

EmailNotificationService extends it and overrides createChannel() to return an EmailChannel. SMSNotificationService returns a SMSChannel. The base class's send() doesn't care which one — it just calls dispatch() on whatever it gets. The if ladder is gone, and each subclass only carries the dependencies its channel actually needs.

Real-World Analogy

A logistics company. Headquarters defines the workflow: pick up the package, transport it, get a signature on delivery. That workflow is the same in every city. But each regional office decides what kind of vehicle to dispatch — a truck in the suburbs, a scooter downtown, a drone in the mountains.

Headquarters never says "use a scooter." It says "create your local vehicle." The choice is delegated to the office that knows the terrain. Add a new region next year? You don't reopen the headquarters playbook — you just open a new office that returns its own vehicle.

Structure

Factory Method pattern: a Creator base class declares a factory method that ConcreteCreators override to return different ConcreteProducts.
Factory Method: the base Creator defines a workflow; subclasses pick the product.

Four roles you'll see in every Factory Method implementation:

  • Product — the interface every created object agrees on. Here: NotificationChannel.
  • Concrete Product — the specific things that get made: EmailChannel, SMSChannel.
  • Creator — the base class that uses the product but doesn't know which concrete type. It declares (often as abstract) the factory method. Here: NotificationService.
  • Concrete Creator — the subclass that overrides the factory method to return a specific Concrete Product. Here: EmailNotificationService, SMSNotificationService.

The trick: the Creator's other methods call the factory method. They don't construct anything directly. That's what lets the same workflow stay shared across subclasses.

Code Examples

Here's the same notification workflow in five languages. Watch how the base class's send() method is identical regardless of which channel a subclass produces.

interface NotificationChannel {
  dispatch(user: User, body: string): Promise<void>;
}

abstract class NotificationService {
  protected abstract createChannel(): NotificationChannel;

  async send(user: User, message: string): Promise<void> {
    const body = this.render(message, user);
    await this.createChannel().dispatch(user, body);
    this.log(user, message);
  }

  protected render(msg: string, user: User): string { /* ... */ return msg; }
  protected log(user: User, msg: string): void      { /* ... */ }
}

class EmailNotificationService extends NotificationService {
  protected createChannel() { return new EmailChannel(); }
}

class SMSNotificationService extends NotificationService {
  protected createChannel() { return new SMSChannel(); }
}
from abc import ABC, abstractmethod

class NotificationChannel(ABC):
    @abstractmethod
    def dispatch(self, user, body): ...

class NotificationService(ABC):
    @abstractmethod
    def create_channel(self) -> NotificationChannel: ...

    def send(self, user, message):
        body = self._render(message, user)
        self.create_channel().dispatch(user, body)
        self._log(user, message)

    def _render(self, msg, user): return msg
    def _log(self, user, msg):    pass

class EmailNotificationService(NotificationService):
    def create_channel(self): return EmailChannel()

class SMSNotificationService(NotificationService):
    def create_channel(self): return SMSChannel()
public interface NotificationChannel {
    void dispatch(User user, String body);
}

public abstract class NotificationService {
    protected abstract NotificationChannel createChannel();

    public final void send(User user, String message) {
        String body = render(message, user);
        createChannel().dispatch(user, body);
        log(user, message);
    }

    protected String render(String msg, User user) { return msg; }
    protected void log(User user, String msg)      { /* ... */ }
}

public final class EmailNotificationService extends NotificationService {
    @Override
    protected NotificationChannel createChannel() {
        return new EmailChannel();
    }
}

public final class SMSNotificationService extends NotificationService {
    @Override
    protected NotificationChannel createChannel() {
        return new SMSChannel();
    }
}
<?php

namespace App\Notifications;

interface NotificationChannel
{
    public function dispatch(User $user, string $body): void;
}

abstract class NotificationService
{
    abstract protected function createChannel(): NotificationChannel;

    final public function send(User $user, string $message): void
    {
        $body = $this->render($message, $user);
        $this->createChannel()->dispatch($user, $body);
        $this->log($user, $message);
    }

    protected function render(string $msg, User $user): string { return $msg; }
    protected function log(User $user, string $msg): void      { /* ... */ }
}

final class EmailNotificationService extends NotificationService
{
    protected function createChannel(): NotificationChannel
    {
        return new EmailChannel();
    }
}

final class SMSNotificationService extends NotificationService
{
    protected function createChannel(): NotificationChannel
    {
        return new SMSChannel();
    }
}
package notifications

type NotificationChannel interface {
    Dispatch(user User, body string) error
}

// In Go, "Factory Method" is usually expressed by injecting the constructor
// as a function rather than overriding a method on a base struct.
type NotificationService struct {
    createChannel func() NotificationChannel
}

func NewEmailNotificationService() *NotificationService {
    return &NotificationService{createChannel: func() NotificationChannel { return EmailChannel{} }}
}

func NewSMSNotificationService() *NotificationService {
    return &NotificationService{createChannel: func() NotificationChannel { return SMSChannel{} }}
}

func (s *NotificationService) Send(user User, message string) error {
    body := render(message, user)
    if err := s.createChannel().Dispatch(user, body); err != nil {
        return err
    }
    logSent(user, message)
    return nil
}

Notice how the Go version sidesteps inheritance entirely — Go doesn't have it. Instead, the "factory method" becomes a function-typed field that the constructor wires in. Same shape, different mechanics. The pattern is about who decides what gets created, not about classes.

When to Use It

Reach for Factory Method when you can answer "yes" to any of these:

  • Your base class has a workflow that constructs an object whose exact type it shouldn't know. Notification senders, document parsers, report renderers — anywhere "the steps are fixed but the thing varies."
  • You want subclasses to extend a workflow without rewriting it. Template Method's first cousin: shared skeleton, swappable construction step.
  • You need to test the workflow without the real product. Override createChannel() in your test to return a FakeChannel — no mocking framework required.
  • You want each subclass to carry only the dependencies it actually needs. EmailNotificationService injects Mailer. SMSNotificationService injects Twilio. Neither has to know about the other.

If you're constructing one object in one place and you're not subclassing, you don't need Factory Method — you just need a regular function or a DI container.

Pros and Cons

Pros

  • Subclasses can swap which concrete product gets used without touching the workflow that consumes it.
  • The Open/Closed Principle in action: adding a channel is a new subclass, never an edit to existing ones.
  • Each Concrete Creator carries its own dependencies. No shared union-type constructor.
  • Easy to drop in test doubles by overriding the factory method.

Cons

  • More classes. If you only ever have one product type, this is overkill — just new it.
  • The hierarchy can multiply: each new product variant might want its own Concrete Creator subclass too.
  • In dynamic languages or DI-heavy codebases, Strategy + a container resolution often does the same job with less ceremony.

Pro Tips

  • Start with a default implementation, not an abstract method. If 80% of subclasses use the same product, default to it in the base class — let subclasses override only when they need to. Cleaner for the common case.
  • Pair Factory Method with Template Method. Template Method gives you the shared workflow shape; Factory Method gives you the swappable creation step inside it. They're often the same class.
  • Keep the factory method's signature minimal. It returns a Product. It doesn't take a config object. If you need configuration, that belongs in the constructor of the concrete product — not as a parameter list on the factory method.
  • Don't confuse Factory Method with Abstract Factory. Factory Method = one method on a class that returns one product. Abstract Factory = a separate class whose whole job is to produce a family of related products. If you're making one thing, you want Factory Method.

Relations with Other Patterns

  • Strategy is the runtime cousin: Factory Method picks the product at subclass-definition time (compile-time in static languages); Strategy picks at call-time via constructor injection. Many codebases evolve from Factory Method to Strategy as DI containers take over.
  • Abstract Factory is the family-of-products generalisation. When you need to create multiple related objects together (a database driver and its query builder and its migration runner), Abstract Factory composes several Factory Methods.
  • Template Method and Factory Method live in the same neighborhood: both let a base class define a workflow with holes for subclasses to fill. Template Method's holes are arbitrary steps; Factory Method's hole is "what kind of object should we make?"
  • Prototype is the alternative to factories when construction is expensive but cloning is cheap.

Final Tips

The cleanest use of Factory Method I've ever shipped was a report-renderer hierarchy. The base Report class handled fetching data, applying filters, paginating, and writing the output stream — about 200 lines, all reusable. Each report type (SalesReport, InventoryReport, UserActivityReport) was a 40-line subclass that only overrode createFormatter() to return its own Formatter. New reports took an afternoon to add, and the shared workflow stayed under one roof.

That's the promise: shared structure, swappable production. Reach for it when you find yourself copy-pasting a workflow because one line in the middle keeps changing.