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
- If there's no real shared procedure around the creation step — just "pick an implementation and delegate to it entirely" — that's Strategy, not Factory Method. The tell is whether the base class actually does something itself (validation, shared setup) beyond calling the factory method.
- For a one-off object with no family of variants, a plain constructor or a static factory method is simpler — Factory Method earns its complexity when subclasses genuinely need to swap the product without touching the shared algorithm.
- If the "family of related objects" needs to stay consistent as a set (not just one product at a time), that's Abstract Factory once it lands, not Factory Method.
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
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of
Reusable Object-Oriented Software. Addison-Wesley. — Chapter 3 formalizes Factory Method;
the book's own running example (a document editor deferring which
Documentsubclass to create) is the direct ancestor of this module's structure. - Liskov, B., & Wing, J. (1994). "A Behavioral Notion of Subtyping." ACM Transactions on
Programming Languages and Systems, 16(6), 1811–1841. — every concrete product returned by a
factory method must be usable anywhere the base
Producttype is expected; that's exactly the substitutability this paper formalizes, and exactly what letsNotificationCreator.send()andPaymentProviderCreator.charge()stay ignorant of which concrete type they got back.
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);
}
}