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

  1. The problem in PideYa
  2. First step: the simple factory (which is not a GoF pattern)
  3. The Factory Method pattern: GoF structure
  4. Complete Java implementation
  5. Modern variants: generics and lambdas
  6. When to use it and when not to
  7. Relation to other patterns
  8. 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 switch blocks. 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 channel

What 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 new or a simple factory is enough. Remember the lesson's ladder: direct new → 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 NotificationService had 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 injected Supplier does the same job with one class fewer.
  • Leaving the switch alive inside the "factory method". A createNotifier() with a switch inside 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, not getNotifier): "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.

© Copyright 2026. All rights reserved