← back to blog

Understanding Factory Pattern

What's the factory pattern? And when should we use it? During my college, I never heard about the factory pattern. The first time I heard about it was when I had already been working for almost 2 years. The mind blowing part? I had already implemented it without knowing it was using the factory pattern!

So what's the factory pattern? Let me give you a real-life example. When you go to a warteg (or a restaurant) and order food, the kitchen has already prepared it. You don't need to know how the food was created, you just ask for what you want, and they provide it. It's similar in your code, you say "I need this type of object" and the factory provides it to you.

Key Ideas

When to use the factory pattern?

When not to use it?

Example in Go

Without Factory Pattern

func main() {
    paymentMethod := "BANK_TRANSFER"

    if paymentMethod == "BANK_TRANSFER" {
        p := BankTransfer{}
        p.Pay(500)
    } else if paymentMethod == "VIRTUAL_ACCOUNT" {
        p := VirtualAccount{}
        p.Pay(500)
    }
}

Problems

With Factory Pattern

factory-pattern

First, we create an interface to define the contract. Everything that implements Pay() is a PaymentMethod:

type PaymentMethod interface {
    Pay(amount int)
}

In Go, the factory pattern uses interfaces as return types.

Now, let's create the implementations:

type BankTransfer struct{}

func (b BankTransfer) Pay(amount int) {
    fmt.Println("Pay using Bank Transfer with amount", amount)
}
type VirtualAccount struct{}

func (v VirtualAccount) Pay(amount int) {
    fmt.Println("Pay using Virtual Account with amount", amount)
}

Create the factory function:

func PaymentMethodFactory(paymentMethod string) PaymentMethod {
    switch paymentMethod {
    case "BANK_TRANSFER":
        return BankTransfer{}
    case "VIRTUAL_ACCOUNT":
        return VirtualAccount{}
    default:
        return nil
    }
}

Now, using the factory is simple:

func main() {
    paymentMethod := PaymentMethodFactory("BANK_TRANSFER")
    if paymentMethod == nil {
        fmt.Println("invalid payment method")
        return
    }

    paymentMethod.Pay(500)
}

Notice that main() only knows about the PaymentMethod interface, it doesn't care about how payment method was created.

Adding a New Payment Method

To add a new payment method, we only need to:

  1. Create the new implementation:
type CreditCard struct{}

func (c CreditCard) Pay(amount int) {
    fmt.Println("Pay using Credit Card with amount", amount)
}
  1. Update the factory function:
func PaymentMethodFactory(paymentMethod string) PaymentMethod {
    switch paymentMethod {
    case "BANK_TRANSFER":
        return BankTransfer{}
    case "VIRTUAL_ACCOUNT":
        return VirtualAccount{}
    case "CREDIT_CARD":
        return CreditCard{}
    default:
        return nil
    }
}

Notice that main() stays unchanged! This is the Open-Closed Principle in action: open for extension, closed for modification.

Benefits of Using Factory Pattern

Conclusion

The factory pattern is a powerful design pattern that helps you write more maintainable and extensible code. While it might seem like over-engineering for simple cases, it becomes invaluable as your codebase grows and you need to support multiple object types.

Sources: