Low Level Design
Factory Pattern — Centralise Object Creation in Java
How the Factory Method pattern eliminates scattered `new` calls, enforces the Open/Closed Principle, and keeps object creation logic in one place.
What is the Factory Pattern?#
The Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses (or a dedicated factory class) to decide which concrete type gets instantiated.
It encapsulates object creation logic in a separate method or class, letting the system create objects based on given parameters without the caller knowing the concrete type.
You reach for Factory when:
- The exact type of object to create isn't known until runtime
- Object creation logic is complex, repetitive, or needs to be centralised
- You want to follow Open/Closed Principle — open for extension, closed for modification
Real-World Analogy#
Think of a notification desk in a company that sends messages to users. Initially the desk only sends emails. As new channels appear — SMS, Push, Slack, WhatsApp — the desk gets overwhelmed creating each type manually.
Instead of the desk handling creation, a Notification Factory produces the correct notification object for each channel. The desk just says "give me a Slack notifier" and uses it.
Class Diagram#
Violation Code — The Problem#
class NotificationService {
public void sendNotification(String type, String message) {
if (type.equals("EMAIL")) {
EmailNotification email = new EmailNotification();
email.send(message);
} else if (type.equals("SMS")) {
SMSNotification sms = new SMSNotification();
sms.send(message);
} else if (type.equals("PUSH")) {
PushNotification push = new PushNotification();
push.send(message);
} else if (type.equals("SLACK")) {
SlackNotification slack = new SlackNotification();
slack.send(message);
}
// Adding WhatsApp means touching this method again ❌
}
}
Issues:
- Tight coupling — caller depends on every concrete notification class
- Violates Open/Closed — adding a new channel forces modifying NotificationService
- Code duplication — creation logic is scattered and repeated
- Violates SRP — the service handles both business logic and object construction
- Hard to test — can't substitute mock objects easily
Enhanced Code — Factory Pattern#
// Product interface
public interface Notification {
void send(String message);
}
// Concrete products
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending EMAIL: " + message);
}
}
public class SMSNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending PUSH: " + message);
}
}
public class SlackNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending SLACK: " + message);
}
}
// The Factory — centralises creation
public class NotificationFactory {
public static Notification createNotification(String type) {
switch (type.toUpperCase()) {
case "EMAIL": return new EmailNotification();
case "SMS": return new SMSNotification();
case "PUSH": return new PushNotification();
case "SLACK": return new SlackNotification();
default: throw new IllegalArgumentException("Unknown type: " + type);
}
}
}
// Service — only knows about the interface, not concrete types
public class NotificationService {
public void sendNotification(String type, String message) {
Notification notification = NotificationFactory.createNotification(type);
notification.send(message);
}
}
Now adding WhatsAppNotification means:
- Create WhatsAppNotification implements Notification
- Add one case "WHATSAPP" to the factory
NotificationService is never touched.
Common LLD Problems Using Factory Pattern#
1. Notification Sender#
- Factory: NotificationFactory.create("email" | "sms" | "push")
- Context: Create the right handler without exposing instantiation logic.
2. Payment Gateway Integration#
- Factory: PaymentFactory.getPayment("upi" | "credit_card" | "paypal")
- Products: UPIPayment, CreditCardPayment, PayPalPayment
- Context: Abstract away payment processor initialization.
3. Document / File Parser#
- Factory: ParserFactory.getParser("pdf" | "docx" | "csv")
- Products: PDFParser, DocxParser, CSVParser
- Context: Read files of various formats without hardcoding parser logic.
4. Food Ordering System#
- Factory: PizzaFactory.create("margherita" | "veggie" | "pepperoni")
- Context: Decouple pizza creation from ordering logic.
5. Database Connection Provider#
- Factory: ConnectionFactory.getConnection("mysql" | "postgres" | "mongodb")
- Context: Unified access to different databases without changing business logic.
6. Shape Drawing Tool#
- Factory: ShapeFactory.create("circle" | "rectangle" | "triangle")
- Context: Create different shape objects in a diagramming or drawing tool.
7. Vehicle Manufacturing#
- Factory: VehicleFactory.getVehicle("car" | "bike" | "truck")
- Context: Manage object creation for different vehicle types uniformly.
8. Cloud Provider SDK#
- Factory: CloudFactory.getProvider("aws" | "gcp" | "azure")
- Products: AWSClient, GCPClient, AzureClient
- Context: Simplify instantiation of cloud-specific clients.
Factory vs Abstract Factory#
| Factory | Abstract Factory | |
|---|---|---|
| Creates | One product type | Families of related products |
| Scope | Single object | Multiple related objects |
| Use when | You need one kind of object | Products must work together |
| References | Links |
|---|---|
| Article Reference | Refactoring Guru — Factory Method |