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 implementation —
Payment, notCreditCardPayment - 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| PIThe 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 : createsThe four components:
| Component | Role | Example |
|---|---|---|
| Product Interface | The contract every product must satisfy | Payment |
| Concrete Product | A real implementation of the interface | CreditCardPayment, EWalletPayment |
| Factory Interface | The contract for all factories | PaymentFactory |
| Concrete Factory | Creates a specific concrete product | CreditCardFactory, 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 #
| Aspect | Simple Factory | Factory Method |
|---|---|---|
| Adding a new type | Change the existing switch function | Create a new factory, leave the old ones alone |
| Open-Closed Principle | Violated — the function must be modified | Satisfied — extension without modification |
| Testability | Hard to mock — the factory is a function | Easy to mock — the factory is an interface |
| Complexity | Low — one function | Higher — many structs |
| Best for | Stable types, small systems | Types 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]
endGo 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.RoundTripperare 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.