Factory and abstract factory
Part of the free Low-Level Design course on LogicWiz, in the chapter: Creational patterns. The class-diagram exercises run in the app.
Week 5: Eleven places that build a notifier
"Product wants WhatsApp alerts by Friday. I searched the code: eleven classes build an SMS notifier by hand."
Arjun, at Monday stand-up
The problem: the same choice in eleven places
SwiftKart tells customers about their orders by SMS, email or push notification. Each customer picks a channel. Here is the code Arjun found in OrderService. Ten other classes have a copy of it.
Notifier notifier;
if (user.preferredChannel().equals("SMS")) {
notifier = new SmsNotifier(config.smsApiKey(), config.smsSenderId());
} else if (user.preferredChannel().equals("EMAIL")) {
notifier = new EmailNotifier(config.smtpHost(), config.smtpFrom());
} else {
notifier = new PushNotifier(config.firebaseKey());
}
notifier.send(user, "Your order is on the way");
Two decisions are copied eleven times:
- Which class to create, based on the customer's channel.
- How to create it: which keys and settings each class needs.
Adding WhatsApp means eleven edits. Miss one, and some customers never hear about their order. The copies also drift apart over time. One class checks for "sms" in lower case, and those customers get push messages instead. Every one of those classes also depends on three concrete notifier classes it does not need to know about.
A simple factory: decide in one place
A factory is an object or method whose job is to create other objects. Callers ask the factory for "a notifier for this channel". They never call new on a concrete notifier class.
public enum Channel { SMS, EMAIL, PUSH, WHATSAPP }
public class NotifierFactory {
private final AppConfig config;
public NotifierFactory(AppConfig config) {
this.config = config;
}
public Notifier forChannel(Channel channel) {
return switch (channel) {
case SMS -> new SmsNotifier(config.smsApiKey(), config.smsSenderId());
case EMAIL -> new EmailNotifier(config.smtpHost(), config.smtpFrom());
case PUSH -> new PushNotifier(config.firebaseKey());
case WHATSAPP -> new WhatsAppNotifier(config.whatsAppToken());
};
}
}
Each caller now has one line: factory.forChannel(user.preferredChannel()).send(user, text).
Notice that the switch did not disappear. The goal is not zero switch statements. The goal is one switch, in one place, owned by one class. Because the switch covers an enum, Java also checks that every channel has a case. Add WHATSAPP to the enum without a case, and this code stops compiling.
Does the factory break the open/closed principle from Week 4? A little: it changes when a channel is added. That is accepted. The factory is the one place that is allowed to know every concrete class. Pass the factory in through the constructor, like any other dependency, so a test can give callers a factory that returns fakes.
Some teams replace the switch with a map from Channel to a function that creates the notifier. That helps when new channels are registered at runtime, for example by plugins. For four fixed channels, the switch is easier to read.
A note on names. People use "factory" loosely. The book Design Patterns (1994, by four authors often called the Gang of Four) describes two creation patterns with "factory" in the name: factory method and abstract factory. The class above is usually called a simple factory. It is useful, but it is neither of those two.
Factory method means a base class calls an abstract method to get an object, and each subclass overrides that method to choose the class.
public abstract class Campaign {
public void run(List<User> users, String text) {
Notifier notifier = createNotifier();
for (User user : users) {
notifier.send(user, text);
}
}
protected abstract Notifier createNotifier();
}
public class SmsCampaign extends Campaign {
protected Notifier createNotifier() {
return new SmsNotifier(Config.smsApiKey(), Config.smsSenderId());
}
}
The base class runs the shared steps; the subclass picks one part. It needs inheritance, so it fits best when you already have a family of subclasses. You will see this shape again with template method in Week 8.
Abstract factory: parts that must match
SwiftKart supports two payment providers, Razorpay and PayU. For each provider, checkout needs three objects:
- a charger that takes the payment,
- a refunder that returns money,
- a webhook verifier that checks the provider's callback messages are genuine.
These must come from the same provider. A Razorpay charger with a PayU webhook verifier fails, and often only in production. An abstract factory is an interface with one creation method per product in a family. Each concrete factory creates a matching set.
public interface PaymentProviderFactory {
Charger createCharger();
Refunder createRefunder();
WebhookVerifier createWebhookVerifier();
}
public class RazorpayFactory implements PaymentProviderFactory {
private final RazorpayClient client;
public RazorpayFactory(RazorpayClient client) {
this.client = client;
}
public Charger createCharger() { return new RazorpayCharger(client); }
public Refunder createRefunder() { return new RazorpayRefunder(client); }
public WebhookVerifier createWebhookVerifier() { return new RazorpayWebhookVerifier(client); }
}
classDiagram
class PaymentProviderFactory {
<<interface>>
+createCharger() Charger
+createRefunder() Refunder
}
class Charger {
<<interface>>
}
PaymentProviderFactory <|.. RazorpayFactory
PaymentProviderFactory <|.. PayUFactory
Charger <|.. RazorpayCharger
Charger <|.. PayUCharger
RazorpayFactory ..> RazorpayCharger : creates
PayUFactory ..> PayUCharger : creates
How do you tell an abstract factory apart from a simple factory? Count the kinds of product. A simple factory makes one kind of thing, such as notifiers. An abstract factory makes several kinds that belong together.
main picks one factory at startup, from configuration. After that, no code can mix parts from two providers. The cost: adding a new product to the family, such as a settlement report reader, changes the interface and every factory.
When a plain constructor is better
A candidate in a mock interview said, "I will add a factory for every class. Interviewers like patterns." The interviewer asked why CartFactory existed. There is one Cart class, and every caller wants the same thing. new Cart(userId) is shorter and clearer.
Use a factory when at least one of these is true:
- Which class to create depends on runtime data, such as the customer's channel.
- The same creation logic would otherwise repeat in several places.
- Creating the object needs setup that callers should not know, such as keys or connection pools.
- Several objects must come from one matching family.
If none is true, call the constructor. Adding a factory later is a small change, so you lose nothing by waiting. You will meet factories again in the problem library, in the notification system and the payment problems.
⚠️ Warning: A factory that takes a
Stringsuch as "sms" and returnsnullfor unknown values moves a typo to runtime. The crash happens far away, at the firstsend. Take an enum, or throw a clear exception for unknown input.
📌 Summary: A factory puts the choice of concrete class in one place, so callers depend only on an interface. Abstract factory creates matching families. Use a plain constructor when the choice never varies.
What Arjun learns next
Choosing the class was one problem. Building one object with many optional parts is another. Next, Arjun meets an Order constructor with nine arguments, and learns the builder and the real costs of a singleton.
Check your understanding
- Eleven classes contain the same if/else that creates an SMS, email or push notifier. What does a notifier factory change?
- new Cart(userId) appears in two places. There is only one Cart class and no setup. Should Arjun add a CartFactory?
- Which situation fits an abstract factory best?
- NotifierFactory.create(String type) returns null when the type is unknown. What is the main problem?