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

  • Encapsulate object creation The place where objects are created is centralized in the factory. All the complex creation logic lives in one place.

  • Client code doesn't know implementation details The main code talks to the interface, not the specific class. This promotes loose coupling and makes your code more maintainable.

  • Easy to extend It's easy to add new types, just update the factory. Your existing client code doesn't need to change.

When to use the factory pattern?

  • You need many types of objects that share a common interface
  • You want to hide creation details from client code
  • You expect to add new types of objects without breaking existing code
  • Object creation logic is complex or involves multiple steps

When not to use it?

  • Object creation is simple and unlikely to change
  • You don't need the abstraction, it will just add unnecessary complexity
  • You only have one or two types of objects (over-engineering)

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

  • Hard to scale: Every time we add a new payment method, we must update the logic in multiple places
  • Violates Open-Closed Principle: We're modifying existing code instead of extending it
  • Tight coupling: The client code is directly dependent on concrete implementations

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

  • Reduces if-else statements everywhere: Centralizes conditional logic in one place
  • Makes code extensible: Easy to add new types without modifying existing code
  • Follows Open-Closed Principle: Open for extension, closed for modification
  • Improves testability: Easy to mock and test different implementations
  • Promotes loose coupling: Client code depends on abstractions, not concrete classes

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: