Skip to main content

Command Palette

Search for a command to run...

Factory Design Pattern

Updated
5 min read
Factory Design Pattern

One common problem in software development is object creation. In small applications, creating objects using the new keyword is straightforward. However, as applications grow, object creation logic often becomes scattered across multiple classes and services.

When different parts of the system need to decide which implementation to create, the code becomes harder to maintain, test, and extend.

The Factory Design Pattern helps solve this problem by centralizing object creation logic and hiding implementation details from the client.

The Problem

Consider a credit card processing system that supports multiple card types:

  • Visa

  • MasterCard

  • American Express

  • RuPay

Without a factory, the application might contain code like this:

if(type == CreditCardType.VISA) {
    card = new VisaCreditCard();
}
else if(type == CreditCardType.AMEX) {
    card = new AmexCreditCard();
}
else if(type == CreditCardType.MASTERCARD) {
    card = new MasterCardCreditCard();
}
else {
    card = new RupayCreditCard();
}

Initially this may seem reasonable.

However, as the application grows, this logic often gets duplicated across multiple services:

PaymentService
BillingService
RewardService
CustomerService

Now every service knows:

  • Which concrete classes exist

  • How objects are created

  • Which implementation should be used

This creates tight coupling between business logic and object creation logic.

Why Is This a Problem?

Code Duplication

The same object creation logic appears in multiple places.

Tight Coupling

Client code becomes dependent on concrete implementations.

new VisaCreditCard()
new AmexCreditCard()

instead of depending on abstractions.

Harder Maintenance

Adding a new card type requires updating object creation logic everywhere it exists.

Difficult Testing

Directly creating concrete objects makes mocking and testing more difficult.

The Factory Solution:

Instead of creating objects directly, we delegate object creation to a factory.

The client asks the factory for an object.

CreditCard card =
    CreditCardFactory.create(type);

The factory decides which implementation should be returned.

The client only interacts with the CreditCard interface.

Implementation

Step 1: Create a Common Interface

public interface CreditCard {

    String getType();

    String getExpiry();
}

This interface defines the behavior that all credit cards must provide.

Step 2: Create Concrete Implementations

Amex Credit Card

public class AmexCreditCard implements CreditCard {

    @Override
    public String getType() {
        return "Amex";
    }

    @Override
    public String getExpiry() {
        return "2030-11-12";
    }
}

Visa Credit Card

public class VisaCreditCard implements CreditCard {

    @Override
    public String getType() {
        return "Visa";
    }

    @Override
    public String getExpiry() {
        return "2033-11-12";
    }
}

Similarly, we can create:

MasterCardCreditCard
RupayCreditCard

All implementations follow the same contract.

Step 3: Create an Enum

public enum CreditCardType {

    AMEX,
    MASTERCARD,
    VISA,
    RUPAY
}

This prevents invalid card types from being passed into the factory.

Step 4: Create the Factory

public class CreditCardFactory {

    public static CreditCard create(
            CreditCardType type) {

        return switch(type) {

            case AMEX ->
                new AmexCreditCard();

            case MASTERCARD ->
                new MasterCardCreditCard();

            case VISA ->
                new VisaCreditCard();

            case RUPAY ->
                new RupayCreditCard();
        };
    }
}

The factory contains all object creation logic in a single place.

Step 5: Client Usage

public class Main {

    public static void main(String[] args) {

        CreditCard card1 =
            CreditCardFactory.create(
                CreditCardType.AMEX);

        CreditCard card2 =
            CreditCardFactory.create(
                CreditCardType.VISA);

        System.out.println(
            card1.getType());

        System.out.println(
            card2.getType());
    }
}

Notice that the client never creates a concrete card directly.

The client only works with the abstraction.

How the Flow Works

Client
   |
   v
CreditCardFactory
   |
   +----> AmexCreditCard
   |
   +----> VisaCreditCard
   |
   +----> MasterCardCreditCard
   |
   +----> RupayCreditCard

The client does not need to know which implementation gets created.

That responsibility belongs entirely to the factory.

Benefits of Using Factory Pattern

Centralized Object Creation

All creation logic exists in one place.

CreditCardFactory

instead of being scattered throughout the application.

Reduced Coupling

The client depends on:

CreditCard

rather than:

VisaCreditCard
AmexCreditCard
MasterCardCreditCard

This makes the code more flexible.

Easier Maintenance

If object creation changes, only the factory needs to be updated.

Business logic remains untouched.

Better Testability

Clients can work against interfaces.

This makes mocking and dependency injection easier.

Limitations :

The Factory Pattern is not free.

It introduces an additional layer of abstraction.

For small applications:

new VisaCreditCard()

may be simpler than creating a factory.

Another common issue is large switch statements.

switch(type) {
   ...
}

As the number of implementations grows, the factory can become harder to maintain.

In such cases, more advanced patterns like Factory Method or Abstract Factory may be better choices.

Factory vs Factory Method vs Abstract Factory

These terms are often confused.

Simple Factory

Our credit card example is a Simple Factory.

CreditCardFactory.create(type)

A single class decides which object to create.

Factory Method

Object creation is delegated to subclasses.

abstract class CreditCardFactory {

    abstract CreditCard createCard();
}
class VisaFactory
        extends CreditCardFactory {

    CreditCard createCard() {
        return new VisaCreditCard();
    }
}

Each factory subclass decides which product to create.

Abstract Factory

Abstract Factory creates families of related objects.

Example:

IndiaPaymentFactory
    ├── RupayCreditCard
    └── RupayDebitCard

USPaymentFactory
    ├── VisaCreditCard
    └── VisaDebitCard

Instead of creating one object, it creates multiple related objects.

When Should You Use Factory Pattern?

Use it when:

  • Multiple implementations exist

  • Object creation logic is duplicated

  • Client code should not know implementation details

  • Object creation becomes more complex over time

Examples include:

Payment Gateways
Notification Systems
Storage Providers
Database Drivers
Messaging Systems

Engineering Takeaways

The Factory Design Pattern is fundamentally about separating object creation from business logic.

Its primary goal is not to avoid the new keyword. The real value comes from reducing coupling and centralizing decisions about which implementation should be instantiated.

A useful rule of thumb is simple:

If multiple parts of the application are repeatedly deciding which implementation to create, that decision likely belongs inside a factory.

This keeps business logic focused on behavior while the factory remains responsible for construction.