If this module had to be boiled down to a single pattern, it would be this one: Factory Method is the most direct answer to the question that opened the module — who decides which concrete class gets instantiated? — and the gateway to the whole creational mindset. In this lesson we will build it on top of PideYa's notification system (push, SMS, email), distinguish it from its popular but uncataloged cousin (the simple factory), and finish with the modern variants that Java's generics and lambdas have brought to the pattern.
Contents
- The problem in PideYa
- First step: the simple factory (which is not a GoF pattern)
- The Factory Method pattern: GoF structure
- Complete Java implementation
- Modern variants: generics and lambdas
- When to use it and when not to
- Relation to other patterns
- Common mistakes, exercises, and conclusion
The problem in PideYa
Recall the symptom from the module introduction: PideYa notifies its customers through whichever channel each one prefers, and the decision of which notifier to create is repeated, looking like this, in the order service, the delivery service, and the promotions service:
// Repeated (with small variations that are already diverging) in THREE services:
Notifier n;
switch (customer.getPreferredChannel()) {
case PUSH: n = new PushNotifier(); break;
case SMS: n = new SmsNotifier(twilioKey); break;
case EMAIL: n = new EmailNotifier(smtpServer); break;
default: throw new IllegalArgumentException();
}
n.send(customer, message);The concrete pains:
- Duplication (a DRY violation): adding the WhatsApp channel, which the business just requested, requires locating and editing all three
switchblocks. One of them will be forgotten. - OCP violation: every new channel modifies stable code in several places.
- Construction knowledge scattered around: the Twilio key and the SMTP server show up wherever the block gets copied.
The good news: client code already programs against the Notifier interface to use the object. It only fails at the moment of creating it. We need to extract that moment into a single, extensible place.
First step: the simple factory (which is not a GoF pattern)
Any developer's first reflex is to move the switch into a class with a static method:
public class NotifierFactory {
public static Notifier create(NotificationChannel channel) {
return switch (channel) {
case PUSH -> new PushNotifier();
case SMS -> new SmsNotifier(MessagingConfig.twilioKey());
case EMAIL -> new EmailNotifier(MessagingConfig.smtpServer());
};
}
}
// In all three services, the whole block shrinks to:
Notifier n = NotifierFactory.create(customer.getPreferredChannel());This is called a simple factory (or static factory) and it deserves to be said loud and clear: it is not a pattern from the GoF catalog; it is an idiom, an honest refactoring step. And do not look down on it: it has already solved two of the three pains (the duplication disappears and the construction knowledge is centralized). For a great many real cases, you can stop here, and doing so is an act of healthy anti-over-engineering.
What the simple factory does not solve: the switch still exists — in a single place, but closed. Adding WhatsApp still means modifying the factory (OCP wounded, although the damage is now local), and above all: nobody can extend the channel system without touching the central code — a serious problem if, for example, PideYa's modules are maintained by different teams, or if the notification core is distributed as a library to the restaurant apps. When that extension axis is real, the actual pattern steps in.
The Factory Method pattern: GoF structure
Intent (GoF): 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.
The driving idea: the general code (the creator) contains all the business logic that uses the product, but leaves a gap — a creation method, the factory method — that each subclass fills by deciding the concrete product. It is OCP in its purest form: to add a channel, you add a pair of classes; nothing existing gets modified. And it is the only class-scoped creational pattern: variation is achieved by inheriting.
classDiagram
class NotificationService {
<<abstract>>
+notifyOrderChange(Order)
#createNotifier()* Notifier
}
class PushNotificationService {
#createNotifier() Notifier
}
class SmsNotificationService {
#createNotifier() Notifier
}
class Notifier {
<<interface>>
+send(Customer, String)
}
class PushNotifier {
+send(Customer, String)
}
class SmsNotifier {
+send(Customer, String)
}
NotificationService <|-- PushNotificationService
NotificationService <|-- SmsNotificationService
Notifier <|.. PushNotifier
Notifier <|.. SmsNotifier
NotificationService ..> Notifier : uses what it creates
PushNotificationService ..> PushNotifier : creates
SmsNotificationService ..> SmsNotifier : creates
The four GoF roles, with their embodiment in PideYa:
| GoF role | Purpose | In PideYa |
|---|---|---|
| Product | Interface of what gets manufactured | Notifier |
| ConcreteProduct | Concrete implementations | PushNotifier, SmsNotifier, EmailNotifier |
| Creator | Class (often abstract) with the common logic; declares the factory method | NotificationService |
| ConcreteCreator | Subclass that implements the factory method and picks the product | PushNotificationService, SmsNotificationService, ... |
Complete Java implementation
The product and its implementations (unchanged from what PideYa already had):
public interface Notifier {
void send(Customer customer, String message);
}
public class PushNotifier implements Notifier {
@Override
public void send(Customer customer, String message) {
// Delivery through the push service to the customer's app
System.out.println("[PUSH to " + customer.getDeviceId() + "] " + message);
}
}
public class SmsNotifier implements Notifier {
private final String twilioKey;
public SmsNotifier(String twilioKey) { this.twilioKey = twilioKey; }
@Override
public void send(Customer customer, String message) {
System.out.println("[SMS to " + customer.getPhone() + "] " + message);
}
}
public class EmailNotifier implements Notifier {
private final String smtpServer;
public EmailNotifier(String smtpServer) { this.smtpServer = smtpServer; }
@Override
public void send(Customer customer, String message) {
System.out.println("[EMAIL to " + customer.getEmail() + "] " + message);
}
}The creator: notice that it contains real business logic (composing the message, deciding when to notify, logging the delivery) that is common to all channels. This is exactly where the pattern shines: it is not "a class that only manufactures", it is a class with work of its own that delegates only the instantiation decision:
public abstract class NotificationService {
/** Common business logic: the same for every channel. */
public void notifyOrderChange(Order order) {
String message = composeMessage(order); // common work
Notifier notifier = createNotifier(); // <-- THE factory method
notifier.send(order.getCustomer(), message); // using the product
EventLog.INSTANCE.record( // common work
"Notified order " + order.getId());
}
private String composeMessage(Order order) {
return "Your order from " + order.getRestaurant().getName()
+ " is " + order.getStatus().toFriendlyText();
}
/** The gap each subclass fills: which concrete notifier to use. */
protected abstract Notifier createNotifier();
}The concrete creators, deliberately trivial — their entire reason for existing is one line:
public class PushNotificationService extends NotificationService {
@Override
protected Notifier createNotifier() {
return new PushNotifier();
}
}
public class SmsNotificationService extends NotificationService {
private final String twilioKey;
public SmsNotificationService(String twilioKey) { this.twilioKey = twilioKey; }
@Override
protected Notifier createNotifier() {
return new SmsNotifier(twilioKey); // the construction detail lives here
}
}The client picks (or better, receives injected) the right service and always works against the base class:
NotificationService service = new SmsNotificationService(config.getTwilioKey());
service.notifyOrderChange(order); // without knowing or caring about the channelWhat have we bought? Adding WhatsApp is now a matter of creating WhatsAppNotifier + WhatsAppNotificationService: two new classes, zero modifications. The switch has vanished from the map (at most there remains a single spot in the composition root that chooses which NotificationService to build per customer, and even that spot can be removed with the registry we will see in the variants). And in tests, a TestNotificationService whose factory method returns a spy notifier lets you verify the common logic without sending anything real.
A common variant of the creator: give the factory method a default implementation (for example, return new PushNotifier()) instead of leaving it abstract. That way the creator is usable as-is and subclasses exist only to change the product. Also common is the parameterized factory method, which takes an argument and chooses among several products: halfway toward the simple factory.
Modern variants: generics and lambdas
With generics: the creator knows the exact type
When the creator must return the concrete type without forcing the client to cast:
public abstract class NotificationService<T extends Notifier> {
protected abstract T createNotifier();
}
public class PushNotificationService extends NotificationService<PushNotifier> {
@Override
protected PushNotifier createNotifier() { return new PushNotifier(); }
}Java also allows return type covariance without generics: a subclass can narrow the return type from Notifier to PushNotifier directly. Generics earn their keep when the product type travels through more of the class's methods.
With lambdas: the factory method as an object
Since Java 8, "a method that creates a Notifier" has a name of its own: Supplier<Notifier> (or Function<X, Notifier> if creation needs data). This enables a compositional version of the pattern: instead of one subclass per product, the creator receives the creation function:
public class NotificationService { // no longer abstract, no subclasses!
private final Supplier<Notifier> notifierFactory;
public NotificationService(Supplier<Notifier> notifierFactory) {
this.notifierFactory = notifierFactory;
}
public void notifyOrderChange(Order order) {
Notifier n = notifierFactory.get(); // the "gap", now injected
n.send(order.getCustomer(), composeMessage(order));
}
// ...
}
// Configuration at the composition root: each "subclass" is now one line
var viaPush = new NotificationService(PushNotifier::new);
var viaSms = new NotificationService(() -> new SmsNotifier(config.getTwilioKey()));Notice the conceptual shift: the GoF pattern varies by inheritance (class scope); the lambda variant varies by composition (the factory is injected), aligning with the maxim "composition over inheritance". In modern Java this form is extremely common, and it is the same idea exploited by the factory registry, which turns the residual switch into a map open to extension:
public class NotifierRegistry {
private final Map<NotificationChannel, Supplier<Notifier>> factories =
new EnumMap<>(NotificationChannel.class);
public void register(NotificationChannel channel, Supplier<Notifier> factory) {
factories.put(channel, factory);
}
public Notifier create(NotificationChannel channel) {
Supplier<Notifier> factory = factories.get(channel);
if (factory == null) throw new IllegalArgumentException("Unregistered channel: " + channel);
return factory.get();
}
}
// PideYa startup: registering is ADDING a line, not editing a switch
registry.register(NotificationChannel.PUSH, PushNotifier::new);
registry.register(NotificationChannel.SMS, () -> new SmsNotifier(config.getTwilioKey()));
registry.register(NotificationChannel.EMAIL, () -> new EmailNotifier(config.getSmtpServer()));Every PideYa module (even a third-party plugin) can register its channels without the core knowing about them. The JDK is full of this philosophy: static creation methods like List.of(...), Optional.of(...), or Files.newBufferedReader(...) are relatives of the simple factory, and injectable Supplier-style factories appear all over the streams and collections API.
When to use it and when not to
Use it when:
- A class with common logic cannot (and should not) anticipate the concrete class of the objects it needs, and that axis of variation is real (PideYa's notification channels: real and growing).
- You want third parties or independent modules to extend your system with new products without touching the core (frameworks: it is the pattern of extension hot spots).
- You need the product's construction to sit next to the variant that uses it, not scattered.
Do not use it when:
- There is a single product and no variation in sight: a well-placed
newor a simple factory is enough. Remember the lesson's ladder: directnew→ simple factory → Factory Method; climb a rung only when the current one hurts. - What varies is not one product but whole families of products that must be mutually consistent: that is the territory of the next lesson.
- The complexity lies in the process of construction (many steps and options), not in the choice of class: that calls for Builder.
Relation to other patterns
Mention only, for your mental map: Abstract Factory is usually implemented as a set of factory methods; Template Method is the sibling pattern — notifyOrderChange is in fact a template method whose variable step is creation; Prototype is the alternative that avoids the creator hierarchy by cloning exemplars; and factories are often exposed as a Singleton or, better, injected.
Common Mistakes and Tips
- Calling any class with "Factory" in its name "Factory Method". The static simple factory is not the GoF pattern; using it is no sin (quite the opposite!), but in an interview or a review it pays to distinguish: the GoF pattern implies an extensible creator whose variation decides the product.
- Building creator hierarchies with no common logic. If
NotificationServicehad no work of its own, the whole hierarchy would be ceremony: the pattern pays off when the creator contributes logic reused by every variant. Without it, an injectedSupplierdoes the same job with one class fewer. - Leaving the
switchalive inside the "factory method". AcreateNotifier()with aswitchinside each subclass shows the split into subclasses was never really made. Each ConcreteCreator should be boringly simple. - Ignoring construction with dependencies. Real products need keys, configuration, collaborators. Notice where we put them: in the concrete creator (or in the registered lambda), never in the client or the base class.
- Tip: name factory methods with intent (
createNotifier, notgetNotifier): "create" communicates that each call may manufacture a new instance, while "get" suggests returning something that already exists.
Exercises
Exercise 1: add a channel without touching anything
Starting from the complete implementation in section 4, add the WhatsApp channel (it needs a metaToken to be constructed). Write the new classes and point out which existing files you had to modify.
Exercise 2: refactor to a registry with lambdas
PideYa's "courier alerts" module has this closed factory:
public class CourierAlertFactory {
public static CourierAlert create(AlertType type) {
switch (type) {
case NEW_ORDER: return new NewOrderAlert();
case CANCELLATION: return new CancellationAlert();
default: throw new IllegalArgumentException();
}
}
}Refactor it into a registry based on Supplier<CourierAlert> that lets the future "tips" module register its TipReceivedAlert without editing this class.
Exercise 3: identify the roles
In the JDK, java.util.Calendar.getInstance() returns a GregorianCalendar or another subclass depending on the locale, and in the collections, Iterable.iterator() forces each collection to decide which concrete Iterator it returns. For each case, identify which GoF role each piece plays and reason about which of the two is a canonical Factory Method and which looks more like a simple factory.
Solutions
Solution 1:
public class WhatsAppNotifier implements Notifier {
private final String metaToken;
public WhatsAppNotifier(String metaToken) { this.metaToken = metaToken; }
@Override
public void send(Customer customer, String message) {
System.out.println("[WHATSAPP to " + customer.getPhone() + "] " + message);
}
}
public class WhatsAppNotificationService extends NotificationService {
private final String metaToken;
public WhatsAppNotificationService(String metaToken) { this.metaToken = metaToken; }
@Override
protected Notifier createNotifier() { return new WhatsAppNotifier(metaToken); }
}Existing files modified: none of the pattern's logic. Only the composition root (the place that decides which service each customer gets) will know about the new option, which is exactly the point designed to change. That is OCP at work.
Solution 2:
public class CourierAlertRegistry {
private final Map<AlertType, Supplier<CourierAlert>> factories =
new EnumMap<>(AlertType.class);
public void register(AlertType type, Supplier<CourierAlert> factory) {
factories.put(type, factory);
}
public CourierAlert create(AlertType type) {
Supplier<CourierAlert> f = factories.get(type);
if (f == null) throw new IllegalArgumentException("Unregistered type: " + type);
return f.get();
}
}
// Core, at startup:
registry.register(AlertType.NEW_ORDER, NewOrderAlert::new);
registry.register(AlertType.CANCELLATION, CancellationAlert::new);
// The "tips" module, at ITS startup, without touching the core:
registry.register(AlertType.TIP, TipReceivedAlert::new);(Honest note: adding TIP to the AlertType enum does touch a shared file; if that bothered you, the map key would become a String or a registrable type, with the type-safety cost that implies.)
Solution 3: in Iterable.iterator(): Creator = Iterable/Collection (with plenty of common logic in AbstractCollection that uses the iterator), ConcreteCreator = ArrayList, HashSet..., Product = Iterator, ConcreteProduct = each collection's internal iterator. It is the canonical Factory Method: the subclass decides the product and the common code consumes it. Calendar.getInstance(), on the other hand, is a static method that internally decides which subclass to return based on configuration: despite the illustrious name, it works like a simple factory (there are no creator subclasses deciding the product).
Conclusion
Factory Method has given you the family's fundamental move: extract the instantiation decision into a named point — a creation method — and make that point extensible, whether by inheritance (the GoF form, class scope) or by composition with Supplier and registries (the modern form). You have also learned to put the simple factory in its rightful place: an extremely valuable idiom that is not the pattern, and the first rung of a ladder (new → simple factory → Factory Method) that you only climb when the current rung hurts.
But our factory method manufactures products one at a time, and there are problems where that is not enough: when PideYa crosses borders it will need to create sets of objects that must be mutually consistent — payment gateway, taxes, and receipt from the same country, no mixing. Manufacturing complete families is the job of the Abstract Factory.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
