Factory Method Pattern #

Every time you write if paymentType == "credit_card" { ... } else if paymentType == "ewallet" { ... }, you are merging two things that should stay separate: the decision of what object to create and how that object is used. The more types you add, the longer those conditional blocks grow, and the more places in the codebase must change on every new type. The Factory Method Pattern breaks this cycle — it moves the object-creation decision into one isolated place, so the code that uses the object neither needs to know nor needs to change when a new type is added.

What Is the Factory Method Pattern? #

Factory Method is a creational design pattern that defines an interface or method for creating objects, but leaves the decision of which class or struct to instantiate to its concrete implementations. Code that needs an object only interacts with the interface — it never calls &CreditCardPayment{} or &EWalletPayment{} directly.

Three core properties define Factory Method:

  • Client code knows only the interface, not the implementationPayment, not CreditCardPayment
  • The object creation process is encapsulated in a factory — creation logic lives in one place, not scattered around
  • Adding a new type does not change client code — this is the Open-Closed Principle in real practice

The most important difference between Factory Method and a plain constructor function is the layer of abstraction. A NewPayment(type string) function still forces the client to name the type explicitly. Factory Method reverses that responsibility — the client picks the right factory, and the factory decides what object gets created.

flowchart TD
    subgraph Client
        C[ProcessPayment\\nfunc]
    end

    subgraph Factory Layer
        direction TB
        FI[PaymentFactory\\ninterface]
        CCF[CreditCardFactory]
        EWF[EWalletFactory]
        BNF[BankTransferFactory]
    end

    subgraph Product Layer
        direction TB
        PI[Payment\\ninterface]
        CCP[CreditCardPayment]
        EWP[EWalletPayment]
        BNP[BankTransferPayment]
    end

    C -->|only knows| FI
    C -->|only knows| PI
    FI --> CCF
    FI --> EWF
    FI --> BNF
    CCF -->|creates| CCP
    EWF -->|creates| EWP
    BNF -->|creates| BNP
    CCP -->|implements| PI
    EWP -->|implements| PI
    BNP -->|implements| PI

The Problem It Solves #

Before looking at the implementation, it is important to understand the concrete problem that occurs without Factory Method.

The Problem: Object Creation Scattered Everywhere #

Imagine an e-commerce system that handles payments in many places: the checkout flow, a refund handler, subscription renewal, and an invoice generator. Without a factory, each of these places carries its own object-creation code:

// ANTI-PATTERN: creation logic scattered everywhere

// In the checkout handler
func (h *CheckoutHandler) Process(req CheckoutRequest) error {
    var payment Payment
    if req.Method == "credit_card" {
        payment = &CreditCardPayment{APIKey: "sk_test_xxxx"}
    } else if req.Method == "ewallet" {
        payment = &EWalletPayment{MerchantID: "M123"}
    } else if req.Method == "bank_transfer" {
        payment = &BankTransferPayment{BankCode: "BCA"}
    }
    return payment.Pay(req.Amount)
}

// In the refund handler — the SAME code repeated again
func (h *RefundHandler) Process(req RefundRequest) error {
    var payment Payment
    if req.Method == "credit_card" {
        payment = &CreditCardPayment{APIKey: "sk_test_xxxx"} // duplication
    } else if req.Method == "ewallet" {
        payment = &EWalletPayment{MerchantID: "M123"}        // duplication
    }
    return payment.Refund(req.Amount)
}

// When BankTransfer gets added to refunds, every place must change

When a new payment method arrives — say QRIS — the developer has to find and change every if/else across the codebase. That is fragile and easy to miss.

// CORRECT: creation logic centralized in a factory

// In the checkout handler — clean, knows no concrete classes at all
func (h *CheckoutHandler) Process(factory PaymentFactory, req CheckoutRequest) error {
    payment := factory.CreatePayment()
    return payment.Pay(req.Amount)
}

// In the refund handler — identical, no duplication
func (h *RefundHandler) Process(factory PaymentFactory, req RefundRequest) error {
    payment := factory.CreatePayment()
    return payment.Refund(req.Amount)
}

// Adding QRIS? Just create a QRISFactory — no other code changes

Structure and Components #

Factory Method consists of four components working together. Understanding each role makes it easy to read and write a correct implementation.

classDiagram
    class PaymentFactory {
        <<interface>>
        +CreatePayment() Payment
    }

    class Payment {
        <<interface>>
        +Pay(amount int) error
        +Refund(amount int) error
        +GetStatus(txID string) Status
    }

    class CreditCardFactory {
        +apiKey string
        +CreatePayment() Payment
    }

    class EWalletFactory {
        +merchantID string
        +CreatePayment() Payment
    }

    class CreditCardPayment {
        +apiKey string
        +Pay(amount int) error
        +Refund(amount int) error
        +GetStatus(txID string) Status
    }

    class EWalletPayment {
        +merchantID string
        +Pay(amount int) error
        +Refund(amount int) error
        +GetStatus(txID string) Status
    }

    PaymentFactory <|.. CreditCardFactory
    PaymentFactory <|.. EWalletFactory
    Payment <|.. CreditCardPayment
    Payment <|.. EWalletPayment
    CreditCardFactory ..> CreditCardPayment : creates
    EWalletFactory ..> EWalletPayment : creates

The four components:

ComponentRoleExample
Product InterfaceThe contract every product must satisfyPayment
Concrete ProductA real implementation of the interfaceCreditCardPayment, EWalletPayment
Factory InterfaceThe contract for all factoriesPaymentFactory
Concrete FactoryCreates a specific concrete productCreditCardFactory, EWalletFactory

Full Implementation in Go #

Go has no classes or inheritance, but its implicit interfaces make Factory Method very natural to implement. Let’s build a payment system step by step.

Step 1: Define the Product Interface #

The product interface is the contract every payment implementation must satisfy. The client only interacts with this interface.

package payment

import "fmt"

// Status represents the state of a payment transaction.
type Status string

const (
    StatusPending  Status = "pending"
    StatusSuccess  Status = "success"
    StatusFailed   Status = "failed"
    StatusRefunded Status = "refunded"
)

// Payment is the product interface — every payment method must satisfy this contract.
type Payment interface {
    Pay(amount int) error
    Refund(transactionID string, amount int) error
    GetStatus(transactionID string) (Status, error)
    MethodName() string
}

Step 2: Create Concrete Products #

Each payment method implements the Payment interface in its own way. The implementation details can differ a lot — CreditCard calls an external API, EWallet may have a redirect flow, BankTransfer waits for manual confirmation.

// CreditCardPayment handles card transactions.
type CreditCardPayment struct {
    apiKey     string
    merchantID string
}

func (c *CreditCardPayment) Pay(amount int) error {
    fmt.Printf("[CreditCard] Processing payment of Rp%d via API key %s\n", amount, c.apiKey[:8]+"***")
    // In a real implementation: call the payment gateway API
    return nil
}

func (c *CreditCardPayment) Refund(transactionID string, amount int) error {
    fmt.Printf("[CreditCard] Refunding Rp%d for transaction %s\n", amount, transactionID)
    return nil
}

func (c *CreditCardPayment) GetStatus(transactionID string) (Status, error) {
    // In a real implementation: query the payment gateway
    return StatusSuccess, nil
}

func (c *CreditCardPayment) MethodName() string {
    return "credit_card"
}


// EWalletPayment handles transactions via digital wallets.
type EWalletPayment struct {
    merchantID  string
    callbackURL string
}

func (e *EWalletPayment) Pay(amount int) error {
    fmt.Printf("[EWallet] Creating payment link of Rp%d for merchant %s\n", amount, e.merchantID)
    // In a real implementation: create a payment URL, redirect the user
    return nil
}

func (e *EWalletPayment) Refund(transactionID string, amount int) error {
    fmt.Printf("[EWallet] Requesting refund of Rp%d for transaction %s\n", amount, transactionID)
    return nil
}

func (e *EWalletPayment) GetStatus(transactionID string) (Status, error) {
    return StatusPending, nil
}

func (e *EWalletPayment) MethodName() string {
    return "ewallet"
}


// BankTransferPayment handles manual bank transfers.
type BankTransferPayment struct {
    bankCode      string
    accountNumber string
}

func (b *BankTransferPayment) Pay(amount int) error {
    fmt.Printf("[BankTransfer] Displaying VA %s for bank %s for Rp%d\n",
        b.accountNumber, b.bankCode, amount)
    return nil
}

func (b *BankTransferPayment) Refund(transactionID string, amount int) error {
    fmt.Printf("[BankTransfer] Manual refund of Rp%d — needs finance team confirmation\n", amount)
    return nil
}

func (b *BankTransferPayment) GetStatus(transactionID string) (Status, error) {
    return StatusPending, nil
}

func (b *BankTransferPayment) MethodName() string {
    return "bank_transfer"
}

Step 3: Define the Factory Interface #

The factory interface defines the contract for all factories. The client only holds a reference to this interface.

// PaymentFactory is the factory interface — every factory must be able to create a Payment.
type PaymentFactory interface {
    CreatePayment() Payment
}

Step 4: Create Concrete Factories #

Each factory is responsible for creating one payment type, including supplying all the configuration it needs.

// Config stores all the configuration a factory needs.
type Config struct {
    CreditCardAPIKey     string
    CreditCardMerchantID string
    EWalletMerchantID    string
    EWalletCallbackURL   string
    BankCode             string
    BankAccountNumber    string
}

// CreditCardFactory creates a fully configured CreditCardPayment instance.
type CreditCardFactory struct {
    config Config
}

func NewCreditCardFactory(cfg Config) *CreditCardFactory {
    return &CreditCardFactory{config: cfg}
}

func (f *CreditCardFactory) CreatePayment() Payment {
    return &CreditCardPayment{
        apiKey:     f.config.CreditCardAPIKey,
        merchantID: f.config.CreditCardMerchantID,
    }
}


// EWalletFactory creates a fully configured EWalletPayment instance.
type EWalletFactory struct {
    config Config
}

func NewEWalletFactory(cfg Config) *EWalletFactory {
    return &EWalletFactory{config: cfg}
}

func (f *EWalletFactory) CreatePayment() Payment {
    return &EWalletPayment{
        merchantID:  f.config.EWalletMerchantID,
        callbackURL: f.config.EWalletCallbackURL,
    }
}


// BankTransferFactory creates a BankTransferPayment instance.
type BankTransferFactory struct {
    config Config
}

func NewBankTransferFactory(cfg Config) *BankTransferFactory {
    return &BankTransferFactory{config: cfg}
}

func (f *BankTransferFactory) CreatePayment() Payment {
    return &BankTransferPayment{
        bankCode:      f.config.BankCode,
        accountNumber: f.config.BankAccountNumber,
    }
}

Step 5: Clean Client Code #

Client code works entirely through interfaces — not a single concrete type is mentioned.

// OrderService uses PaymentFactory without knowing the concrete implementation.
type OrderService struct {
    paymentFactory PaymentFactory
}

func NewOrderService(factory PaymentFactory) *OrderService {
    return &OrderService{paymentFactory: factory}
}

func (s *OrderService) Checkout(orderID string, amount int) error {
    payment := s.paymentFactory.CreatePayment()
    fmt.Printf("Checking out order %s using %s\n", orderID, payment.MethodName())
    return payment.Pay(amount)
}

func (s *OrderService) ProcessRefund(orderID, transactionID string, amount int) error {
    payment := s.paymentFactory.CreatePayment()
    return payment.Refund(transactionID, amount)
}

Usage from main.go or a layer above the service:

func main() {
    cfg := payment.Config{
        CreditCardAPIKey:     "sk_test_xxxx",
        CreditCardMerchantID: "M_CC_001",
        EWalletMerchantID:    "M_EW_001",
        EWalletCallbackURL:   "https://myapp.com/payment/callback",
        BankCode:             "BCA",
        BankAccountNumber:    "1234567890",
    }

    // Pick the factory based on configuration or user request
    factories := map[string]payment.PaymentFactory{
        "credit_card":   payment.NewCreditCardFactory(cfg),
        "ewallet":       payment.NewEWalletFactory(cfg),
        "bank_transfer": payment.NewBankTransferFactory(cfg),
    }

    // Dispatch to the right factory
    selectedMethod := "ewallet" // e.g., from the request body
    factory, ok := factories[selectedMethod]
    if !ok {
        log.Fatalf("unknown payment method: %s", selectedMethod)
    }

    svc := NewOrderService(factory)
    if err := svc.Checkout("ORDER-001", 150000); err != nil {
        log.Fatal(err)
    }
}

Simple Factory vs Factory Method #

In Go, you will very often find two approaches both called “factory” even though they differ fundamentally. Understanding the difference matters so you pick the right one.

Simple Factory: A Switch Function #

A Simple Factory is a single function that takes a parameter and returns the matching implementation. This is not the Factory Method Pattern — it is a helper function with conditional logic.

// Simple Factory — a single function with a switch
func NewPayment(method string, cfg Config) (Payment, error) {
    switch method {
    case "credit_card":
        return &CreditCardPayment{
            apiKey:     cfg.CreditCardAPIKey,
            merchantID: cfg.CreditCardMerchantID,
        }, nil
    case "ewallet":
        return &EWalletPayment{
            merchantID:  cfg.EWalletMerchantID,
            callbackURL: cfg.EWalletCallbackURL,
        }, nil
    case "bank_transfer":
        return &BankTransferPayment{
            bankCode:      cfg.BankCode,
            accountNumber: cfg.BankAccountNumber,
        }, nil
    default:
        return nil, fmt.Errorf("unknown payment method: %s", method)
    }
}

Using a Simple Factory:

// The client still names the type explicitly via a string
payment, err := NewPayment("credit_card", cfg)

Head-to-Head Comparison #

AspectSimple FactoryFactory Method
Adding a new typeChange the existing switch functionCreate a new factory, leave the old ones alone
Open-Closed PrincipleViolated — the function must be modifiedSatisfied — extension without modification
TestabilityHard to mock — the factory is a functionEasy to mock — the factory is an interface
ComplexityLow — one functionHigher — many structs
Best forStable types, small systemsTypes that keep growing, modular systems
flowchart TD
    subgraph "Simple Factory"
        SF["NewPayment(method)"]
        SF -->|"credit_card"| SFA[CreditCardPayment]
        SF -->|"ewallet"| SFB[EWalletPayment]
        SF -->|"bank_transfer"| SFC[BankTransferPayment]
        SF -.->|"add QRIS?\\nchange this function"| SFD[???]
    end

    subgraph "Factory Method"
        FMI[PaymentFactory\\ninterface]
        FMI --> FMA[CreditCardFactory]
        FMI --> FMB[EWalletFactory]
        FMI --> FMC[BankTransferFactory]
        FMI --> FMD[QRISFactory\\nadded without\\nchanging anything else]
    end

Go Idiom: The Hybrid Approach

Pragmatic Go code often uses a hybrid — a Simple Factory for dispatching to the right factory, and Factory Method for implementation. The registry pattern is a common example:

With this, adding a new payment method is just RegisterFactory("qris", NewQRISFactory(cfg)) — no other code changes.


Testing with Factory Method #

One of the biggest advantages of Factory Method is how easy it makes testing. Because the client depends on an interface, you can inject a mock factory without changing any production code.

// MockPaymentFactory for testing — no external library needed
type MockPaymentFactory struct {
    payment Payment
}

func (m *MockPaymentFactory) CreatePayment() Payment {
    return m.payment
}

// MockPayment for testing
type MockPayment struct {
    payError    error
    refundError error
    status      Status
    methodName  string
}

func (m *MockPayment) Pay(amount int) error          { return m.payError }
func (m *MockPayment) Refund(txID string, amount int) error { return m.refundError }
func (m *MockPayment) GetStatus(txID string) (Status, error) { return m.status, nil }
func (m *MockPayment) MethodName() string            { return m.methodName }


// Test OrderService without touching a real payment gateway
func TestOrderService_Checkout_Success(t *testing.T) {
    mockPayment := &MockPayment{
        payError:   nil,
        methodName: "mock",
    }
    mockFactory := &MockPaymentFactory{payment: mockPayment}

    svc := NewOrderService(mockFactory)
    err := svc.Checkout("ORDER-TEST-001", 100000)

    if err != nil {
        t.Errorf("expected nil error, got %v", err)
    }
}

func TestOrderService_Checkout_PaymentFailed(t *testing.T) {
    mockPayment := &MockPayment{
        payError:   errors.New("insufficient balance"),
        methodName: "mock",
    }
    mockFactory := &MockPaymentFactory{payment: mockPayment}

    svc := NewOrderService(mockFactory)
    err := svc.Checkout("ORDER-TEST-002", 500000)

    if err == nil {
        t.Error("expected error, got nil")
    }
}

The tests above never touch an external API, need no network connection, and run in milliseconds. That is the direct payoff of an interface-based design.


Factory Method in the Go Ecosystem #

Factory Method is not just theory — it appears everywhere in the Go standard library and popular packages.

database/sql — Driver Selection #

// sql.Open is a factory that picks a driver by name
db, err := sql.Open("postgres", dsn)  // PostgreSQL driver
db, err := sql.Open("mysql", dsn)     // MySQL driver
db, err := sql.Open("sqlite3", dsn)   // SQLite driver

// The client (sql.Open) does not know the driver implementation — only the sql.Driver interface

log/slog — Handler Selection #

// Pick a handler (factory) without changing how you log
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))    // JSON output
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))    // Text output
// Custom handler: implement the slog.Handler interface, done

HTTP Transport #

// http.RoundTripper is the product interface
// Various transports implement it
client := &http.Client{
    Transport: &http.Transport{...},        // default TCP transport
    // or
    Transport: &CustomRetryTransport{...},  // custom with retry logic
    // or
    Transport: &MockTransport{...},         // mock for testing
}

When to Use Each Approach #

Choosing between Simple Factory and Factory Method (or neither) depends on the concrete characteristics of the system being built.

USE a plain constructor (func New...) if:
  ✓ There is only one implementation
  ✓ There is no plan to add variations
  ✓ The creation logic is very simple (one line)

USE Simple Factory if:
  ✓ There are several implementations but the count is stable
  ✓ Small team, fast iteration
  ✓ The system is still in the exploration phase
  ✗ Not if the switch already has more than 5-7 cases

USE Factory Method if:
  ✓ Implementation types will keep growing over time
  ✓ Third parties must be able to add their own implementations
  ✓ Testing and mocking are a priority
  ✓ Plugin-based or extensible architecture
  ✓ Large team with clear interface contracts between teams

Factory Method Review Checklist #

DESIGN:
  □ There is a clear product interface (not a concrete type)
  □ There is a factory interface separate from the product interface
  □ Client code depends only on interfaces, never concrete types
  □ Each factory is responsible for exactly one product type

IMPLEMENTATION:
  □ The factory returns an interface, not a concrete struct
  □ Errors from the creation process are handled properly
  □ Configuration is injected into the factory, not into the concrete product directly
  □ No business logic inside the factory — creation only

EXTENSIBILITY:
  □ Adding a new type requires no changes in client code
  □ No type-based switch or if-else in client code
  □ Open-Closed Principle is satisfied

TESTING:
  □ There is a mock factory for unit tests
  □ All tests run without external dependencies
  □ Edge cases (factory failure, wrong config) are tested

Summary #

  • Factory Method separates “what is created” from “how it is used” — the client only knows the interface, never the concrete implementation.
  • Four main components: Product Interface, Concrete Product, Factory Interface, Concrete Factory — understand each role before writing code.
  • Simple Factory vs Factory Method: use Simple Factory for small systems with stable types; use Factory Method when types keep growing and extensibility is a priority.
  • Open-Closed Principle: adding a new payment method only requires creating a new factory — no other code changes.
  • Testing becomes trivial: since the client depends on an interface, a mock factory can be injected without touching production code at all.
  • The Go ecosystem uses it widely: database/sql, log/slog, http.RoundTripper are Factory Method examples you use every day.
  • The registry pattern is a Go idiom that combines Simple Factory (for dispatch) with Factory Method (for implementation) — flexible and easy to extend.
  • Don’t over-engineer: for a small system with a single implementation, a plain constructor is enough; Factory Method shines when variation and extensibility are genuinely needed.

← Previous: Singleton   Next: Abstract Factory →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact