Skip to content
Commitwise
Menu
Design Patterns

Factory Method

Move object creation into one place, let subclasses or configuration decide which class to create, and use static factory methods for clearer, validated construction, with examples in Swift, Kotlin, Java and TypeScript.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

The app sends notifications by email, SMS and push. Six screens decide which to use, and each has its own if type == "sms" { SmsSender(apiKey) } else …. Then a new channel arrives, and someone updates five of the six. The bug report says “WhatsApp messages from the settings screen go out as emails”. The Factory Method pattern gives object creation one home, so “which class do we create?” is answered in exactly one place.

New to some of these? Open what you need, or skip ahead.

Basics:What is an interface (or protocol)?

An interface (Kotlin, Java) or protocol (Swift) is a list of things a type promises it can do, without saying how. For example, “anything that is a PaymentMethod has a pay(amount) function”.

Different classes can implement the same interface in different ways (card, PayPal, cash), and code that only knows the interface can use any of them. That’s what lets you swap one implementation for another, for example a fake one in tests.

Basics:What is a design pattern?

A design pattern is a named, reusable solution to a problem that keeps coming up in software design, like “swap one algorithm for another” or “notify many objects of a change”. It’s not a library you install; it’s a way of arranging classes and interfaces that experienced developers recognize.

Knowing the names helps in two ways: you recognize the pattern in code you read (Android, iOS and Spring use many of them), and you can discuss designs quickly in reviews and interviews (“let’s use a Strategy here”).

Basics:What is inheritance?

Inheritance lets one class build on another: class Square extends Rectangle (Java, TypeScript), class Square : Rectangle() (Kotlin), class Square: Rectangle (Swift). The subclass gets the parent’s properties and methods and can override some of them to behave differently.

Code that expects a Rectangle also accepts a Square. Whether that’s actually safe is what the “L” in SOLID is about.

Part Not started

Creation in one place

When you order a pizza, you say “a large margherita”. You don’t tell the kitchen which oven to use or how to stretch the dough. The kitchen decides how to make it; you only say what you want and get a pizza back.

A factory is that kitchen for objects. Code asks for “a sender for SMS” and gets back something it can use to send. Which class that is, and how it’s set up (API keys, settings), is decided in one place.

“Factory” covers a family of ideas: a simple factory (one function that picks a concrete type), the GoF Factory Method (a creator class defers instantiation to an overridable method), static factory methods (named constructors like List.of, URL(string:)) and Abstract Factory (a family of related products). They share one move: callers depend on an abstraction and a creation function, never on concrete constructors, so the set of products can change in one place. This part covers the simple factory; the GoF form is next.

Before: Every screen decides by itself

// In the settings screen, the checkout screen, the reminders job…
let sender: NotificationSender
if channel == "sms" {
    sender = SmsSender(apiKey: Secrets.smsKey)
} else if channel == "push" {
    sender = PushSender(appId: Config.pushAppId)
} else {
    sender = EmailSender(host: Config.smtpHost)
}
// In the settings screen, the checkout screen, the reminders job…
val sender: NotificationSender = when (channel) {
    "sms" -> SmsSender(apiKey = Secrets.smsKey)
    "push" -> PushSender(appId = Config.pushAppId)
    else -> EmailSender(host = Config.smtpHost)
}
// In the settings screen, the checkout screen, the reminders job…
NotificationSender sender;
if (channel.equals("sms")) sender = new SmsSender(Secrets.smsKey());
else if (channel.equals("push")) sender = new PushSender(Config.pushAppId());
else sender = new EmailSender(Config.smtpHost());
// In the settings screen, the checkout screen, the reminders job…
const sender: NotificationSender =
  channel === "sms" ? new SmsSender(secrets.smsKey)
  : channel === "push" ? new PushSender(config.pushAppId)
  : new EmailSender(config.smtpHost);

After: One factory; callers only ask

enum Channel: String { case email, sms, push }

struct SenderFactory {
    let config: Config

    func sender(for channel: Channel) -> any NotificationSender {
        switch channel {
        case .email: EmailSender(host: config.smtpHost)
        case .sms:   SmsSender(apiKey: config.smsKey)
        case .push:  PushSender(appId: config.pushAppId)
        }
    }
}

let sender = factory.sender(for: .sms)        // callers never name SmsSender
enum class Channel { EMAIL, SMS, PUSH }

class SenderFactory(private val config: Config) {
    fun sender(channel: Channel): NotificationSender = when (channel) {
        Channel.EMAIL -> EmailSender(config.smtpHost)
        Channel.SMS -> SmsSender(config.smsKey)
        Channel.PUSH -> PushSender(config.pushAppId)
    }
}

val sender = factory.sender(Channel.SMS)      // callers never name SmsSender
enum Channel { EMAIL, SMS, PUSH }

final class SenderFactory {
    private final Config config;
    SenderFactory(Config config) { this.config = config; }

    NotificationSender sender(Channel channel) {
        return switch (channel) {
            case EMAIL -> new EmailSender(config.smtpHost());
            case SMS -> new SmsSender(config.smsKey());
            case PUSH -> new PushSender(config.pushAppId());
        };
    }
}

NotificationSender sender = factory.sender(Channel.SMS);   // callers never name SmsSender
type Channel = "email" | "sms" | "push";

class SenderFactory {
  constructor(private readonly config: Config) {}

  sender(channel: Channel): NotificationSender {
    switch (channel) {
      case "email": return new EmailSender(this.config.smtpHost);
      case "sms": return new SmsSender(this.config.smsKey);
      case "push": return new PushSender(this.config.pushAppId);
    }
  }
}

const sender = factory.sender("sms");         // callers never name SmsSender

What changed:

  • One place decides. Adding WhatsApp means one new case in the factory, and the compiler points at it (an enum and a switch without default must handle every case).
  • Callers depend on the interface (NotificationSender), not on SmsSender; they don’t need to know about API keys or hosts either.
  • No silent fallback. The old else turned every unknown or misspelled channel into email.
Spot the bugWhatsApp messages sent as emails

Tap the line with the bug.

func sender(for channel: String) -> any NotificationSender {
    switch channel {
    case "sms":  return SmsSender(apiKey: config.smsKey)
    case "push": return PushSender(appId: config.pushAppId)
    default:     return EmailSender(host: config.smtpHost)
    }
}
fun sender(channel: String): NotificationSender = when (channel) {
    "sms" -> SmsSender(config.smsKey)
    "push" -> PushSender(config.pushAppId)
    else -> EmailSender(config.smtpHost)
}
NotificationSender sender(String channel) {
    return switch (channel) {
        case "sms" -> new SmsSender(config.smsKey());
        case "push" -> new PushSender(config.pushAppId());
        default -> new EmailSender(config.smtpHost());
    };
}
function sender(channel: string): NotificationSender {
  switch (channel) {
    case "sms": return new SmsSender(config.smsKey);
    case "push": return new PushSender(config.pushAppId);
    default: return new EmailSender(config.smtpHost);
  }
}

Practice: testing with a factory

Because the factory is the one place that creates senders, a test can give the code a factory that returns fakes:

  1. Describe the factory with an interface: protocol SenderMaking { func sender(for: Channel) -> any NotificationSender }.
  2. Pass it in to the class that sends notifications, instead of creating SenderFactory inside it.
  3. In tests, pass a FakeSenderFactory that returns a RecordingSender for every channel.
  4. Assert on what the recording sender received: “one SMS to +381…, with this text”.

Static factory methods

Static factory methods (Effective Java, item 1) are the most common factory in everyday code: List.of(…), Optional.empty(), URL(string:), Duration.ofSeconds(…). Compared with constructors they can:

  • have names that say what they make (Money.ofCents(1999) vs Money(1999)),
  • return nothing or an error for invalid input (Swift failable init?, a Result, a nullable),
  • return a cached or shared instance (Boolean.valueOf, Integer.valueOf for -128…127),
  • return a subtype (List.of picks an immutable implementation by size),
  • hide the class entirely (Kotlin operator fun invoke in a companion, an interface with a companion factory).
  • Factories centralize “which concrete type, configured how”. Callers depend on the interface and the factory.
  • Take a closed type (enum, sealed, union) and switch exhaustively; parse raw strings at the boundary; never fall back silently.
  • Static factories: names, validation/failure (init?, Result), caching, subtype selection, hidden implementations.
  • Inject the factory (an interface) so tests can supply fakes.

Check this part

  1. What does a factory centralize?

  2. A factory has else -> EmailSender. What's the risk?

  3. Which is an advantage of a static factory method over a constructor?

When several places decide which class to create, they drift apart. A factory puts that decision in one function that returns an interface, so callers never name the concrete classes, and an unknown type fails loudly instead of falling back silently.

Part Not started

The classic form: let the subclass decide

A chain of cafés has one recipe for “serve a drink”: take the order, make the drink, put it in a cup, hand it over. Every café does the first and last steps the same way. But the Italian café makes espresso, and the tea house makes tea. Head office writes the routine; each café fills in “make the drink”.

In the classic Factory Method, a base class has the routine, and calls a method (the factory method) to create the one object that differs. Each subclass overrides that method to create its own kind.

GoF Factory Method: a Creator with a template operation that uses a Product, obtained by calling an abstract or overridable createProduct(); ConcreteCreators override it. It’s Template Method applied to instantiation. In modern code, the same flexibility is usually achieved by composition: pass the product, or a factory function, into the creator. The classic form remains common in frameworks, where you subclass to plug in (onCreateViewHolder, makeUIView, createConnection).

protocol ReportWriter {
    func write(_ rows: [[String]]) -> Data
}

class ReportExporter {
    func makeWriter() -> any ReportWriter {             // the factory method
        fatalError("Subclasses must override makeWriter()")
    }

    final func export(_ rows: [[String]]) -> Data {     // the shared routine
        let writer = makeWriter()
        return writer.write(rows)
    }
}

final class CsvExporter: ReportExporter {
    override func makeWriter() -> any ReportWriter { CsvWriter() }
}

final class JsonExporter: ReportExporter {
    override func makeWriter() -> any ReportWriter { JsonWriter() }
}
interface ReportWriter {
    fun write(rows: List<List<String>>): ByteArray
}

abstract class ReportExporter {
    protected abstract fun makeWriter(): ReportWriter    // the factory method

    fun export(rows: List<List<String>>): ByteArray {    // the shared routine
        val writer = makeWriter()
        return writer.write(rows)
    }
}

class CsvExporter : ReportExporter() {
    override fun makeWriter(): ReportWriter = CsvWriter()
}

class JsonExporter : ReportExporter() {
    override fun makeWriter(): ReportWriter = JsonWriter()
}
interface ReportWriter {
    byte[] write(List<List<String>> rows);
}

abstract class ReportExporter {
    protected abstract ReportWriter makeWriter();        // the factory method

    public final byte[] export(List<List<String>> rows) { // the shared routine
        ReportWriter writer = makeWriter();
        return writer.write(rows);
    }
}

final class CsvExporter extends ReportExporter {
    @Override protected ReportWriter makeWriter() { return new CsvWriter(); }
}

final class JsonExporter extends ReportExporter {
    @Override protected ReportWriter makeWriter() { return new JsonWriter(); }
}
interface ReportWriter {
  write(rows: string[][]): Uint8Array;
}

abstract class ReportExporter {
  protected abstract makeWriter(): ReportWriter;          // the factory method

  export(rows: string[][]): Uint8Array {                  // the shared routine
    const writer = this.makeWriter();
    return writer.write(rows);
  }
}

class CsvExporter extends ReportExporter {
  protected makeWriter(): ReportWriter { return new CsvWriter(); }
}

class JsonExporter extends ReportExporter {
  protected makeWriter(): ReportWriter { return new JsonWriter(); }
}

export is written once, in the base class. It doesn’t know whether it’s making CSV or JSON: it asks makeWriter(), and each subclass answers differently. Swift has no abstract keyword, so the base class traps if a subclass forgets to override; Kotlin, Java and TypeScript make the method abstract, and the compiler enforces it.

Predict the output

What does this print?

let exporter: ReportExporter = JsonExporter()
print(type(of: exporter.makeWriter()) == JsonWriter.self ? "json" : "csv")
val exporter: ReportExporter = JsonExporter()
println(if (exporter.export(rows).first() == '['.code.toByte()) "json" else "csv")
ReportExporter exporter = new JsonExporter();
System.out.println(exporter.export(rows)[0] == '[' ? "json" : "csv");
const exporter: ReportExporter = new JsonExporter();
console.log(exporter.export(rows)[0] === "[".charCodeAt(0) ? "json" : "csv");
Exerciseeasy

Add an Excel export

Product wants an .xlsx export next to CSV and JSON. Using the classic version above, what do you add, and what doesn’t change? Then rewrite the exporter without subclasses: ReportExporter receives what it needs to create a writer.

›

In the classic version, you add one subclass and one writer. Without subclasses, pass the writer itself, or a function that creates one, into the exporter’s constructor.

SolutionTry it yourself first

Classic: add XlsxWriter and XlsxExporter overriding makeWriter(). ReportExporter.export doesn’t change.

Without subclasses: the exporter takes a writer factory (a function), and each format is a value, not a class:

final class ReportExporter {
    private let makeWriter: () -> any ReportWriter
    init(makeWriter: @escaping () -> any ReportWriter) { self.makeWriter = makeWriter }

    func export(_ rows: [[String]]) -> Data { makeWriter().write(rows) }
}

let xlsx = ReportExporter(makeWriter: { XlsxWriter() })
class ReportExporter(private val makeWriter: () -> ReportWriter) {
    fun export(rows: List<List<String>>): ByteArray = makeWriter().write(rows)
}

val xlsx = ReportExporter(::XlsxWriter)
final class ReportExporter {
    private final Supplier<ReportWriter> makeWriter;
    ReportExporter(Supplier<ReportWriter> makeWriter) { this.makeWriter = makeWriter; }

    byte[] export(List<List<String>> rows) { return makeWriter.get().write(rows); }
}

ReportExporter xlsx = new ReportExporter(XlsxWriter::new);
class ReportExporter {
  constructor(private readonly makeWriter: () => ReportWriter) {}
  export(rows: string[][]): Uint8Array { return this.makeWriter().write(rows); }
}

const xlsx = new ReportExporter(() => new XlsxWriter());

Both versions keep export closed to change. The second needs no class per format and is easier to test (pass a fake writer), which is why modern code reaches for it first. Why create a new writer per export instead of passing one writer? If writers hold state (a buffer), a fresh one per call is safer; if they’re stateless, passing the writer directly is simpler still.

Where you meet it

Framework hookProduct
RecyclerView.Adapter.onCreateViewHolderA view holder per view type
SwiftUI UIViewRepresentable.makeUIViewThe UIKit view to host
UICollectionViewDiffableDataSource cell providerA configured cell (a factory closure)
Spring FactoryBean.getObject, @Bean methodsBeans the container manages
JDBC DataSource.getConnectionA connection (pooled or not)

Prefer composition for your own code: subclassing a creator to change one product couples the subclass to the creator’s template and multiplies classes (CSV × compressed × encrypted). Pass factories (functions or small interfaces) instead, and keep the classic form for framework extension points that require it.

  • GoF Factory Method = Template Method for instantiation: the creator’s routine calls an overridable makeProduct().
  • Framework hooks use it (onCreateViewHolder, makeUIView, @Bean); in your code, pass a factory function or object instead of subclassing.
  • Swift lacks abstract: use protocols with requirements, or pass closures, rather than fatalError overrides.
let exporter = ReportExporter(makeWriter: CsvWriter.init)
val exporter = ReportExporter(::CsvWriter)
var exporter = new ReportExporter(CsvWriter::new);
const exporter = new ReportExporter(() => new CsvWriter());

Check this part

  1. In the classic Factory Method, who decides which product is created?

  2. Adding an Excel export in the classic version requires changing…

  3. What's the common modern alternative to subclassing a creator?

In the classic Factory Method, a base class does its work with a product it creates through an overridable method, and each subclass decides which product to create. Modern code often passes a creation function or an object instead of subclassing.

Part Not started

Registries and cached products

When the set of products is open (plugins, feature modules, configuration from a server), a switch in one file can’t list them all. A registry maps a key to a function that creates the product, and each module registers its own:

final class SenderRegistry {
    private var makers: [String: () -> any NotificationSender] = [:]

    func register(_ channel: String, _ make: @escaping () -> any NotificationSender) {
        precondition(makers[channel] == nil, "Channel \(channel) registered twice")
        makers[channel] = make
    }

    func sender(for channel: String) throws -> any NotificationSender {
        guard let make = makers[channel] else { throw SenderError.unknownChannel(channel) }
        return make()
    }
}

registry.register("whatsapp") { WhatsAppSender(token: config.whatsAppToken) }   // in the WhatsApp module
class SenderRegistry {
    private val makers = mutableMapOf<String, () -> NotificationSender>()

    fun register(channel: String, make: () -> NotificationSender) {
        check(channel !in makers) { "Channel $channel registered twice" }
        makers[channel] = make
    }

    fun sender(channel: String): NotificationSender =
        makers[channel]?.invoke() ?: throw IllegalArgumentException("Unknown channel $channel")
}

registry.register("whatsapp") { WhatsAppSender(config.whatsAppToken) }   // in the WhatsApp module
final class SenderRegistry {
    private final Map<String, Supplier<NotificationSender>> makers = new ConcurrentHashMap<>();

    void register(String channel, Supplier<NotificationSender> make) {
        if (makers.putIfAbsent(channel, make) != null)
            throw new IllegalStateException("Channel " + channel + " registered twice");
    }

    NotificationSender sender(String channel) {
        Supplier<NotificationSender> make = makers.get(channel);
        if (make == null) throw new IllegalArgumentException("Unknown channel " + channel);
        return make.get();
    }
}

registry.register("whatsapp", () -> new WhatsAppSender(config.whatsAppToken()));   // in the WhatsApp module
class SenderRegistry {
  private readonly makers = new Map<string, () => NotificationSender>();

  register(channel: string, make: () => NotificationSender): void {
    if (this.makers.has(channel)) throw new Error(`Channel ${channel} registered twice`);
    this.makers.set(channel, make);
  }

  sender(channel: string): NotificationSender {
    const make = this.makers.get(channel);
    if (!make) throw new Error(`Unknown channel ${channel}`);
    return make();
  }
}

registry.register("whatsapp", () => new WhatsAppSender(config.whatsAppToken));   // in the WhatsApp module

The registry knows nothing about WhatsApp; the WhatsApp module registers itself at startup. Two safety rules are built in: registering the same key twice is an error (two modules fighting over “sms” is a bug you want to see), and asking for an unknown key throws instead of returning a default.

DI containers are registries with lifecycle: Spring collects every NotificationSender bean into a Map<String, NotificationSender> keyed by bean name; Hilt multibindings (@IntoMap with a @StringKey) do the same at compile time; ServiceLoader (Java) discovers implementations from the classpath. Prefer the container’s mechanism over a hand-written static registry, which reintroduces global mutable state and registration-order bugs.

Production puzzles

Spot the bugHeaders that leak between requests

Tap the line with the bug.

final class RequestOptions {
    var headers: [String: String] = ["Accept": "application/json"]
    var timeout: TimeInterval = 10

    private static let standard = RequestOptions()
    static func makeDefault() -> RequestOptions { standard }
}

let options = RequestOptions.makeDefault()
options.headers["Authorization"] = "Bearer \(adminToken)"
class RequestOptions {
    val headers = mutableMapOf("Accept" to "application/json")
    var timeoutSeconds = 10

    companion object {
        private val standard = RequestOptions()
        fun default(): RequestOptions = standard
    }
}

val options = RequestOptions.default()
options.headers["Authorization"] = "Bearer $adminToken"
final class RequestOptions {
    final Map<String, String> headers = new HashMap<>(Map.of("Accept", "application/json"));
    int timeoutSeconds = 10;

    private static final RequestOptions STANDARD = new RequestOptions();
    static RequestOptions defaults() { return STANDARD; }
}

RequestOptions options = RequestOptions.defaults();
options.headers.put("Authorization", "Bearer " + adminToken);
class RequestOptions {
  headers: Record<string, string> = { Accept: "application/json" };
  timeoutSeconds = 10;

  private static readonly standard = new RequestOptions();
  static defaults(): RequestOptions { return RequestOptions.standard; }
}

const options = RequestOptions.defaults();
options.headers["Authorization"] = "Bearer " + adminToken;

Exercisemedium

Payment providers by country

Checkout must use Stripe in most countries, a local provider in Serbia and Brazil, and a “pay on delivery” option where cards aren’t supported. The mapping comes from remote config and changes without app releases; providers live in separate modules; tests must run without any real provider. Design the creation side: what’s the factory’s input and output, where does the mapping live, and what happens for a country nobody configured?

›

Separate three decisions: which provider key a country uses (config), how to build each provider (registered creators), and what to do when either is missing (an explicit fallback policy, not a hidden else).

SolutionTry it yourself first
  • Interface: PaymentProvider with authorize, capture, refund; checkout depends only on it.
  • Registry of creators: each provider module registers "stripe", "local-rs", "cod" with a creator function (or via the DI container’s map multibinding). Creators receive their own config (keys, endpoints) from the container, not from global state.
  • Routing config: remote config maps country → ordered list of provider keys (RS: ["local-rs", "stripe"]), validated when it’s fetched: every key must exist in the registry, or the config is rejected and the last good one is kept.
  • Factory: providerFor(country) reads the routing, takes the first key whose provider is registered and enabled, and returns it. For an unconfigured country it applies an explicit default ("stripe"), logs a metric and alerts, so a missing market is visible.
  • Tests: register fake creators under the same keys; routing and fallback logic are tested without any SDK.
  • Lifecycle: providers that hold connections are created once per app (singleton scope in the container) and reused; per-request state goes in method parameters, never in the provider.
  • Open sets of products: a registry of key → creator, registered by modules; duplicate keys and unknown keys fail loudly. Prefer DI container multibindings or ServiceLoader over hand-written static registries.
  • Cache only immutable products (List.of, Integer.valueOf); a factory that returns a shared mutable object leaks changes across callers.
  • Validate remote routing config against the registry; make fallbacks explicit and observable.

Check this part

  1. When is a registry better than a switch inside the factory?

  2. What should a registry do when asked for an unknown key?

  3. A factory method returns the same cached RequestOptions object every time. When is that safe?

When new kinds are added by other modules or by configuration, a registry maps a key to a creator function. Factories should create fresh objects (or immutable shared ones), never hand out a cached mutable object that callers then change.

Key takeaways

  • A factory puts “which class to create, and how” in one place; callers get an interface back.
  • Use an enum and an exhaustive switch, and fail loudly on unknown input instead of falling back silently.
  • In the classic Factory Method, a base class’s routine calls an overridable method, and each subclass creates its own product.
  • Passing a creation function into a class gives the same flexibility without subclasses.
  • Factories, Factory Method, static factories and Abstract Factory all decouple callers from concrete construction.
  • Static factories add names, validation, caching and subtype selection; cache only immutable products.
  • Prefer composition (factory functions, DI multibindings) to creator subclasses outside framework hooks.
  • Open product sets need registries with loud failures and validated, observable configuration.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.