Ilya Brin - Software Engineer

History is written by its contributors

Factory Pattern in Go: Creating Objects with Factories

2025-06-10 5 min read Patterns Ilya Brin

Factory Pattern solves one problem: how to create objects without being tied to concrete types. Instead of directly calling a constructor, use a factory function that decides which object to create.

Problem: Tight Coupling to Types

Without a factory, code is tied to concrete types:

type MySQLDatabase struct{}
type PostgresDatabase struct{}

func main() {
    db := &MySQLDatabase{} // Tight coupling
    // To change DB, need to change code
}

Every time you change the type, you have to rewrite code.

Solution: Factory Function

type Database interface {
    Connect() error
    Query(sql string) ([]Row, error)
}

func NewDatabase(dbType string) Database {
    switch dbType {
    case "mysql":
        return &MySQLDatabase{}
    case "postgres":
        return &PostgresDatabase{}
    default:
        return &MySQLDatabase{}
    }
}

Now the type is determined in one place. Client code doesn’t know about concrete implementations.

Simple Factory: Basic Factory

The simplest variant - a function that returns an interface:

type Logger interface {
    Log(message string)
}

type FileLogger struct {
    path string
}

func (f *FileLogger) Log(message string) {
    // Write to file
}

type ConsoleLogger struct{}

func (c *ConsoleLogger) Log(message string) {
    fmt.Println(message)
}

func NewLogger(logType string) Logger {
    if logType == "file" {
        return &FileLogger{path: "/var/log/app.log"}
    }
    return &ConsoleLogger{}
}

Usage:

logger := NewLogger("console")
logger.Log("Application started")

Factory Method: Factory in Interface

When you need to delegate object creation to subclasses:

type Notification interface {
    Send(message string) error
}

type NotificationFactory interface {
    CreateNotification() Notification
}

type EmailFactory struct{}

func (e *EmailFactory) CreateNotification() Notification {
    return &EmailNotification{}
}

type SMSFactory struct{}

func (s *SMSFactory) CreateNotification() Notification {
    return &SMSNotification{}
}

Usage:

func SendAlert(factory NotificationFactory, msg string) {
    notification := factory.CreateNotification()
    notification.Send(msg)
}

Abstract Factory: Object Families

When you need to create related objects:

type UIFactory interface {
    CreateButton() Button
    CreateCheckbox() Checkbox
}

type WindowsFactory struct{}

func (w *WindowsFactory) CreateButton() Button {
    return &WindowsButton{}
}

func (w *WindowsFactory) CreateCheckbox() Checkbox {
    return &WindowsCheckbox{}
}

type MacFactory struct{}

func (m *MacFactory) CreateButton() Button {
    return &MacButton{}
}

func (m *MacFactory) CreateCheckbox() Checkbox {
    return &MacCheckbox{}
}

Usage:

func RenderUI(factory UIFactory) {
    button := factory.CreateButton()
    checkbox := factory.CreateCheckbox()
    
    button.Render()
    checkbox.Render()
}

Factory with Configuration

type ServerConfig struct {
    Host string
    Port int
    TLS  bool
}

func NewServer(config ServerConfig) *Server {
    server := &Server{
        host: config.Host,
        port: config.Port,
    }
    
    if config.TLS {
        server.setupTLS()
    }
    
    return server
}

Factory with Validation

func NewUser(email, password string) (*User, error) {
    if !isValidEmail(email) {
        return nil, errors.New("invalid email")
    }
    
    if len(password) < 8 {
        return nil, errors.New("password too short")
    }
    
    return &User{
        email:    email,
        password: hashPassword(password),
    }, nil
}

Factory Registration

For extensibility without code changes:

type PaymentFactory func() Payment

var factories = make(map[string]PaymentFactory)

func RegisterPayment(name string, factory PaymentFactory) {
    factories[name] = factory
}

func CreatePayment(name string) Payment {
    factory, ok := factories[name]
    if !ok {
        return nil
    }
    return factory()
}

func init() {
    RegisterPayment("stripe", func() Payment {
        return &StripePayment{}
    })
    RegisterPayment("paypal", func() Payment {
        return &PayPalPayment{}
    })
}

Factory with Object Pool

type ConnectionPool struct {
    connections chan *Connection
}

func NewConnectionPool(size int) *ConnectionPool {
    pool := &ConnectionPool{
        connections: make(chan *Connection, size),
    }
    
    for i := 0; i < size; i++ {
        pool.connections <- &Connection{}
    }
    
    return pool
}

func (p *ConnectionPool) Get() *Connection {
    return <-p.connections
}

func (p *ConnectionPool) Put(conn *Connection) {
    p.connections <- conn
}

When to Use Factory

1. Creation Depends on Conditions

func NewCache(size int) Cache {
    if size > 1000 {
        return &RedisCache{}
    }
    return &MemoryCache{}
}

2. Complex Initialization

func NewHTTPClient(timeout time.Duration) *http.Client {
    return &http.Client{
        Timeout: timeout,
        Transport: &http.Transport{
            MaxIdleConns:        100,
            MaxIdleConnsPerHost: 10,
            IdleConnTimeout:     90 * time.Second,
        },
    }
}

3. Hide Implementation

func NewMetrics() Metrics {
    return &prometheusMetrics{
        registry: prometheus.NewRegistry(),
    }
}

When NOT to Use Factory

1. Simple Structures

// Not needed
func NewPoint(x, y int) *Point {
    return &Point{x: x, y: y}
}

// Sufficient
point := Point{X: 10, Y: 20}

2. Single Type

// Not needed
func NewUser() *User {
    return &User{}
}

// Sufficient
user := &User{}

3. No Creation Logic

// Not needed
func NewConfig() *Config {
    return &Config{}
}

// Sufficient
config := &Config{}

Factory vs Builder

// Factory: creation in one call
db := NewDatabase("postgres")

// Builder: step-by-step creation
db := NewDatabaseBuilder().
    WithHost("localhost").
    WithPort(5432).
    WithSSL(true).
    Build()

Factory for simple cases. Builder for complex objects with many parameters.

Testing with Factories

type UserRepository interface {
    Save(user *User) error
}

func NewUserRepository(env string) UserRepository {
    if env == "test" {
        return &MockRepository{}
    }
    return &PostgresRepository{}
}

func TestUserService(t *testing.T) {
    repo := NewUserRepository("test")
    service := NewUserService(repo)
    // Test with mock repository
}

Performance

func BenchmarkFactory(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = NewLogger("console")
    }
}

func BenchmarkDirect(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = &ConsoleLogger{}
    }
}

Factory adds minimal overhead. For most cases, the difference is negligible.

Real Example: HTTP Client

type HTTPClient interface {
    Get(url string) (*Response, error)
    Post(url string, body []byte) (*Response, error)
}

func NewHTTPClient(opts ...Option) HTTPClient {
    client := &httpClient{
        timeout: 30 * time.Second,
        retries: 3,
    }
    
    for _, opt := range opts {
        opt(client)
    }
    
    return client
}

type Option func(*httpClient)

func WithTimeout(d time.Duration) Option {
    return func(c *httpClient) {
        c.timeout = d
    }
}

func WithRetries(n int) Option {
    return func(c *httpClient) {
        c.retries = n
    }
}

Usage:

client := NewHTTPClient(
    WithTimeout(10 * time.Second),
    WithRetries(5),
)

Conclusion

Factory Pattern in Go:

  • Use for creating objects with conditions
  • Hide concrete implementations behind interfaces
  • Apply for complex initialization
  • Combine with functional options
  • Avoid for simple structures

Factory isn’t about complication. It’s about flexibility and extensibility.

If object creation is one line, factory isn’t needed. If creation depends on conditions, requires validation, or hides details - factory helps.

comments powered by Disqus