Appearance
Factory Method
GoF Design Patterns — Creational · See also: Builder · Singleton
Introduction
Factory Method is a creational design pattern that defines an interface for creating an object but lets subclasses — or factory classes — decide which concrete class to instantiate. The caller works with the product through a common interface and never references the concrete type directly. This decouples object creation from object use, so adding new product variants requires writing new code rather than editing existing callers.
Intent
Define an interface for creating an object, but let subclasses or factory classes decide which concrete class to instantiate, deferring the instantiation decision to a single place.
Structure
Participants
| Participant | Role |
|---|---|
NotificationFactory | Creator interface — declares the factory method createNotification. |
DefaultNotificationFactory | Concrete creator — implements the factory method, selecting the right concrete product. |
Notification | Product interface — defines the contract all concrete products must satisfy. |
EmailNotification, SmsNotification, PushNotification | Concrete products — specific implementations of the product interface. |
Anti-Pattern — Direct Instantiation
The problem starts small: a NotificationService needs to send an email, so it calls new EmailNotification() directly.
java
// ANTI-PATTERN: NotificationService coupled to concrete EmailNotification
public class NotificationService {
// Hard-coded concrete class — adding SMS requires opening and modifying this class
private final EmailNotification emailNotification = new EmailNotification(
new SmtpClient("smtp.company.com", 587));
public void notifyUser(String userId, String message) {
String emailAddress = lookupEmailAddress(userId);
// This call can only ever send email. No SMS. No push. No Slack.
emailNotification.send(emailAddress, message);
}
public void notifyUserViaSms(String userId, String message) {
// To add SMS we must modify this class and add a new field and method
SmsNotification smsNotification = new SmsNotification(
new SmsGateway("https://sms-api.company.com", "api-key"));
String phone = lookupPhoneNumber(userId);
smsNotification.send(phone, message);
}
// Every new channel forces another modification here.
// This class now owns object creation AND orchestration logic — an SRP violation.
}Problems with this design:
NotificationServiceis compiled againstEmailNotificationandSmsNotification; adding a new channel requires editing and re-testing the service class.- Unit tests for
NotificationServicecannot run without a live SMTP server and SMS gateway. - The service carries both orchestration logic and construction logic — two distinct responsibilities.
- Callers that need different channels must receive different service instances or use conditional branching scattered across the codebase.
Correct Implementation
Extract a Notification interface, move instantiation into a factory, and let the factory decide which concrete class to build.
Step 1 — Product interface
java
// Notification.java — the product abstraction
public interface Notification {
void send(String recipient, String message);
String getChannel();
}Step 2 — Concrete products
java
// EmailNotification.java
public class EmailNotification implements Notification {
private final SmtpClient smtpClient;
public EmailNotification(SmtpClient smtpClient) {
this.smtpClient = smtpClient;
}
@Override
public void send(String recipient, String message) {
smtpClient.sendEmail(recipient, "Notification", message);
System.out.printf("[EMAIL] Sent to %s%n", recipient);
}
@Override
public String getChannel() { return "EMAIL"; }
}
// SmsNotification.java
public class SmsNotification implements Notification {
private final SmsGateway smsGateway;
public SmsNotification(SmsGateway smsGateway) {
this.smsGateway = smsGateway;
}
@Override
public void send(String recipient, String message) {
smsGateway.dispatch(recipient, message);
System.out.printf("[SMS] Sent to %s%n", recipient);
}
@Override
public String getChannel() { return "SMS"; }
}
// PushNotification.java
public class PushNotification implements Notification {
private final PushProvider pushProvider;
public PushNotification(PushProvider pushProvider) {
this.pushProvider = pushProvider;
}
@Override
public void send(String recipient, String message) {
pushProvider.push(recipient, message);
System.out.printf("[PUSH] Sent to device token %s%n", recipient);
}
@Override
public String getChannel() { return "PUSH"; }
}Step 3 — Factory interface and concrete factory
java
// NotificationFactory.java — the creator abstraction
public interface NotificationFactory {
Notification createNotification(String type);
}
// DefaultNotificationFactory.java — concrete creator
public class DefaultNotificationFactory implements NotificationFactory {
private final SmtpClient smtpClient;
private final SmsGateway smsGateway;
private final PushProvider pushProvider;
public DefaultNotificationFactory(SmtpClient smtpClient,
SmsGateway smsGateway,
PushProvider pushProvider) {
this.smtpClient = smtpClient;
this.smsGateway = smsGateway;
this.pushProvider = pushProvider;
}
@Override
public Notification createNotification(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification(smtpClient);
case "SMS" -> new SmsNotification(smsGateway);
case "PUSH" -> new PushNotification(pushProvider);
default -> throw new IllegalArgumentException(
"Unknown notification channel: " + type);
};
}
}Step 4 — Service uses the factory, never new
java
// NotificationService.java — no concrete types, no new keyword
public class NotificationService {
private final NotificationFactory factory;
private final UserRepository userRepository;
public NotificationService(NotificationFactory factory,
UserRepository userRepository) {
this.factory = factory;
this.userRepository = userRepository;
}
public void notify(String userId, String preferredChannel, String message) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));
// Creation is delegated; this class has no idea which concrete class is used
Notification notification = factory.createNotification(preferredChannel);
String recipient = resolveRecipient(user, notification.getChannel());
notification.send(recipient, message);
}
private String resolveRecipient(User user, String channel) {
return switch (channel) {
case "EMAIL" -> user.getEmail();
case "SMS" -> user.getPhone();
case "PUSH" -> user.getDeviceToken();
default -> throw new IllegalStateException("Unresolvable channel: " + channel);
};
}
}Interaction flow
Real-World Examples
Java standard library
java
// Calendar.getInstance() — returns GregorianCalendar, JapaneseImperialCalendar, etc.
// depending on the Locale. The caller never knows which subclass it receives.
Calendar calendar = Calendar.getInstance(Locale.JAPAN);
// JDBC DriverManager — returns the Connection implementation registered
// by whichever JDBC driver (MySQL, PostgreSQL, H2) is on the classpath.
Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost/mydb", "user", "pass");Spring's BeanFactory
Spring's BeanFactory and ApplicationContext are factory method implementations at the framework level. getBean(Class<T> requiredType) returns the registered bean without the caller knowing the concrete class:
java
@Autowired
private ApplicationContext context;
// The factory method — Spring decides which implementation to return
Notification notification = context.getBean(Notification.class);AWS SDK client builders
The AWS SDK uses a builder variant of Factory Method. Each client type has a static builder() factory method that returns a fluent builder for that specific client, insulating the caller from the underlying DefaultSdkHttpClient and transport configuration:
java
// S3Client.create() is a zero-argument factory method
S3Client s3 = S3Client.create();
// SqsClient.builder() is a parameterized factory method
SqsClient sqs = SqsClient.builder()
.region(Region.US_EAST_1)
.credentialsProvider(DefaultCredentialsProvider.create())
.build();
// SNS factory method — topic ARN drives which concrete behavior (FIFO vs standard)
SnsClient sns = SnsClient.builder()
.region(Region.EU_WEST_1)
.build();
// Sending through SNS — the caller does not instantiate any concrete SNS class
sns.publish(PublishRequest.builder()
.topicArn("arn:aws:sns:eu-west-1:123456789012:OrderEvents")
.message("Order confirmed")
.build());When to Use
- A class cannot anticipate the exact type of object it must create at compile time.
- You want subclasses or a dedicated factory to control which class to instantiate.
- You need to centralize and enforce rules around object construction (e.g., connection pooling, caching, default configuration).
- You are building a framework or library where you want consumers to extend creation behaviour without modifying the library's core code.
When Not to Use
- There is only one concrete product and no variation is expected. A factory adds indirection with no benefit.
- The construction logic is trivial and the caller controls all the context needed to call
newsafely.
Consequences
Benefits
| Benefit | Explanation |
|---|---|
| Decouples creation from use | Callers depend on the product interface, not any concrete class. |
| Single place to change | Adding a new channel means writing a new class and updating only the factory — no other code changes. |
| Testability | Inject a mock factory in tests; no real SMTP servers or SMS gateways needed. |
| Follows OCP | New products extend the system without modifying existing, working code. |
Trade-offs
| Trade-off | Explanation |
|---|---|
| Extra abstraction | Two additional types per product family (factory interface + concrete factory). Unnecessary for simple creation. |
| Type safety at boundaries | The createNotification(String type) signature uses strings, requiring validation at runtime. Enums or sealed types mitigate this. |
Factory Method vs. Abstract Factory
Factory Method creates one product — Notification. Abstract Factory creates families of related products — Notification, NotificationTemplate, and NotificationLogger — guaranteeing that all three products from the same factory are compatible with each other.
Use Factory Method when you have one product with varying implementations. Use Abstract Factory when you have multiple products that must be used together.
Related Concepts
- Builder: Factory Method decides which class to instantiate; Builder controls how a complex object is assembled step by step.
- Singleton: A factory method that always returns the same instance combines both patterns — common in connection pool managers.
- Dependency Inversion Principle: Injecting a
NotificationFactoryinterface rather than a concrete factory is DIP applied to creation logic. - Open/Closed Principle: New notification channels extend the factory without modifying existing service classes.
- Clean Architecture: In Clean Architecture, factories often live in the composition root (infrastructure layer) so that inner layers remain free of
new ConcreteClass()calls.