Skip to content

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

ParticipantRole
NotificationFactoryCreator interface — declares the factory method createNotification.
DefaultNotificationFactoryConcrete creator — implements the factory method, selecting the right concrete product.
NotificationProduct interface — defines the contract all concrete products must satisfy.
EmailNotification, SmsNotification, PushNotificationConcrete 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:

  • NotificationService is compiled against EmailNotification and SmsNotification; adding a new channel requires editing and re-testing the service class.
  • Unit tests for NotificationService cannot 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 new safely.

Consequences

Benefits

BenefitExplanation
Decouples creation from useCallers depend on the product interface, not any concrete class.
Single place to changeAdding a new channel means writing a new class and updating only the factory — no other code changes.
TestabilityInject a mock factory in tests; no real SMTP servers or SMS gateways needed.
Follows OCPNew products extend the system without modifying existing, working code.

Trade-offs

Trade-offExplanation
Extra abstractionTwo additional types per product family (factory interface + concrete factory). Unnecessary for simple creation.
Type safety at boundariesThe 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 productNotification. Abstract Factory creates families of related productsNotification, 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.

  • 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 NotificationFactory interface 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.