← All patterns

Factory Method

Creational · view source on GitHub

Read this in: English · Português · Español

Category: Creational

The problem

A class has a fixed procedure to run, but one step of that procedure — which concrete object to create — needs to vary. Hard-coding new ConcreteThing() inside the procedure ties it to one specific subclass, so supporting a new variant means editing code that already works, and the procedure's own logic (validation, shared setup) ends up duplicated in every place that also needs to pick a variant.

The solution

Put the fixed procedure in a base class, and defer the "which object to create" decision to an abstract method that subclasses override. The base class calls its own abstract factory method polymorphically — it never needs to know which concrete product it's actually going to get.

classDiagram
    class Creator {
        +templateOperation()
        #createProduct() Product
    }
    class ConcreteCreatorA
    class ConcreteCreatorB
    class Product {
        <<interface>>
    }
    Creator <|-- ConcreteCreatorA
    Creator <|-- ConcreteCreatorB
    Creator --> Product : creates via factory method

Classic example

classic/NotificationCreator defines send(recipient, message) once — including a validation step every subclass inherits for free — and defers createNotification() to EmailNotificationCreator and SmsNotificationCreator. Neither subclass touches send() itself; they only say which Notification gets built. NotificationCreatorTest checks both concrete creators route to the right notification type, and that the shared validation in the base class applies to both without either subclass having to implement it.

Applied example: payment provider selection

applied/PaymentProviderCreator holds one real shared step — amount validation — and defers createProvider() to PixPaymentProviderCreator, BoletoPaymentProviderCreator, and CreditCardPaymentProviderCreator. PaymentCheckout looks up the right creator by PaymentMethod and calls charge() on it — a real payment gateway adding a fourth method later means adding one new creator class, and it gets the amount-validation step for free, without copying it. PaymentCheckoutTest covers all three providers, the shared validation firing regardless of method, and the unregistered-method failure case.

When not to use it

Test coverage

100% instruction coverage, 100% branch coverage (JaCoCo). Reproduce it yourself:

./gradlew :creational:factorymethod:jacocoTestReport

Report at creational/factorymethod/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/designpatterns/creational/factorymethod/classic/NotificationCreatorTest.java
package com.designpatterns.creational.factorymethod.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class NotificationCreatorTest {

    @Test
    void emailCreatorRoutesThroughAnEmailNotification() {
        NotificationCreator creator = new EmailNotificationCreator();

        String result = creator.send("alice@example.com", "your order shipped");

        assertThat(result).isEqualTo("EMAIL to alice@example.com: your order shipped");
    }

    @Test
    void smsCreatorRoutesThroughAnSmsNotification() {
        NotificationCreator creator = new SmsNotificationCreator();

        String result = creator.send("+15551234567", "your order shipped");

        assertThat(result).isEqualTo("SMS to +15551234567: your order shipped");
    }

    @Test
    void theSharedValidationInSendAppliesToEveryCreatorSubclass() {
        NotificationCreator emailCreator = new EmailNotificationCreator();
        NotificationCreator smsCreator = new SmsNotificationCreator();

        assertThatThrownBy(() -> emailCreator.send("alice@example.com", " "))
                .isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> smsCreator.send("+15551234567", ""))
                .isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> emailCreator.send("alice@example.com", null))
                .isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/designpatterns/creational/factorymethod/applied/PaymentCheckoutTest.java
package com.designpatterns.creational.factorymethod.applied;

import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class PaymentCheckoutTest {

    private final PaymentCheckout checkout = PaymentCheckout.withDefaultCreators();

    @Test
    void routesToThePixProvider() {
        assertThat(checkout.charge(PaymentMethod.PIX, 5_000L))
                .isEqualTo("PIX charge of 5000 cents processed instantly");
    }

    @Test
    void routesToTheBoletoProvider() {
        assertThat(checkout.charge(PaymentMethod.BOLETO, 12_000L))
                .isEqualTo("Boleto issued for 12000 cents, due in 3 business days");
    }

    @Test
    void routesToTheCreditCardProvider() {
        assertThat(checkout.charge(PaymentMethod.CREDIT_CARD, 8_000L))
                .isEqualTo("Credit card charge of 8000 cents authorized");
    }

    @Test
    void theSharedValidationAppliesRegardlessOfMethod() {
        assertThatThrownBy(() -> checkout.charge(PaymentMethod.PIX, 0L))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsAnUnregisteredPaymentMethod() {
        PaymentCheckout partialCheckout = new PaymentCheckout(Map.of(PaymentMethod.PIX, new PixPaymentProviderCreator()));

        assertThatThrownBy(() -> partialCheckout.charge(PaymentMethod.BOLETO, 1_000L))
                .isInstanceOf(IllegalArgumentException.class);
    }
}

View full JaCoCo coverage report →