Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

Functional Options Pattern in Golang

Functional Options Pattern also called just Options Pattern, is a creational design pattern that lets you build a complex struct using a variadic constructor that accepts zero or more functions as arguments. We refer to those functions as options, thus the pattern name.

Various Golang libraries widely adopted the Functional Options Pattern. Golang doesn't support default values, and functional options are one way of providing similar functionality, arguably, more elegant. Although the most common use case is changing a configuration object or providing additional options, we can use functional options to build any complex struct.

I find the Functional Options Pattern particularly useful for creating domain objects where it successfully replaces the need for using a Builder Pattern. In this post, I'm going to demonstrate how you can use the Functional Options Pattern in the context of implementing an Entity.

Minimal requirements for creating a Customer Entity in our imaginary system are identity and email. Our system generates the identity, and we ask user during sign-up to only provide the email.

type Customer struct {
  id    string
  email string
}

func NewCustomer(id, email string) (*Customer, error) {
  return &Customer{id: id, email: email}, nil
}

There's nothing surprising about this code, very typical constructor in Golang.

Optional values

Requirements change, and now we also would like to accept the Customer's name to send more personalized emails. Existing customers that we store already don't have names nor do we want to make it a required field for new customers which could lower the conversion.

type Customer struct {
  id        string
  email     string
  firstName  *string
  lastName  *string
}

The simplest solution would be to also accept the first name and last name as positional arguments in the Customer constructor. The problem with this is that it doesn't scale in real production systems where entities can have dozens of fields, leads to multi-line function calls, and without inline hints in your IDE, it's easy to make mistakes. Another issue with an additional positional argument is that it's a breaking change to the function's signature which means revisiting each place the constructor is used, not fun. In application code that might be just a handful of places like a handler that creates a Customer and a repository implementation that retrieves the Customer from storage. It's also going to be the case for at least dozens of tests that will depend on the Customer entity.

What's an alternative? The Functional Options Pattern!

type Option = func(c *Customer)

func WithName(firstName, lastName *string) Option {
	return func(c *Customer) {
		c.firstName = firstName
		c.lastName = lastName
	}
}

func NewCustomer(id, email string, opts ...Option) (*Customer, error) {
	c := &Customer{id: id, email: email}
	for _, opt := range opts {
		opt(c)
	}
	return c, nil
}

func main() {
	id := "6fa49e0a"
	email := "[email protected]"
	firstName := "Jane"
	lastName := "Doe"
	_, _ = NewCustomer(id, email)
	_, _ = NewCustomer(id, email, WithName(&firstName, &lastName))
}

The Option is a type for a function that accepts a pointer to an empty or minimal struct created in the constructor. Option function starts with With* prefix. This particular example also shows that one functional option doesn't have to set just one field and may be designed so it better reflects what's happening in your domain. The constructor simply composes all functional options.

Default values

We want to introduce a loyalty program and reward each new customer with 100 points for signing up. We can use the Customer model to express that business requirement. As I already mentioned, the Function Options Pattern allows doing that very easily with default values despite Golang not supporting them explicitly.

func WithLoyaltyPoints(loyaltyPoints int) Option {
	return func(c *Customer) {
		c.loyaltyPoints = loyaltyPoints
	}
}

func NewCustomer(id, email string, opts ...Option) (*Customer, error) {
	c := &Customer{id: id, email: email, loyaltyPoints: 100}	for _, opt := range opts {
		opt(c)
	}
	return c, nil
}

func main() {
  id := "6fa49e0a"
  email := "[email protected]"
  firstName := "Jane"
  lastName := "Doe"
  _, _ = NewCustomer(id, email)
  _, _ = NewCustomer(id, email, WithName(&firstName, &lastName),
    WithLoyaltyPoints(200))}

Validation

It's best for our domain objects to always be in a valid state. Such a system design makes it much easier to reason about. This way each time we encounter an object we can be sure it's valid and no further validation by its consumer is needed. We achieve that primary by performing validation within the constructors and diligently using only constructors to create a new instance.

var ErrEmptyId = errors.New("id cannot be empty")
var ErrInvalidEmail = errors.New("email is invalid")
var ErrEmptyFirstName = errors.New("first name cannot be empty")
var ErrEmptyLastName = errors.New("last lane cannot be empty")

func NewCustomer(id, email string, opts ...Option) (*Customer, error) {
	c := &Customer{id: id, email: email, loyaltyPoints: 100}
	for _, opt := range opts {
		opt(c)
	}
	if err := c.validate(); err != nil {		return nil, err	}	return c, nil
}

func (c *Customer) validate() error {
	if c.id == "" {
		return ErrEmptyId
	}
	if !validator.IsValidEmail(c.email) {
		return ErrInvalidEmail
	}
	if c.firstName != nil && *c.firstName == "" {
		return ErrEmptyFirstName
	}
	if c.lastName != nil && *c.lastName == "" {
		return ErrEmptyLastName
	}
	return nil
}

The constructor performs validation after applying function options. Purists will notice that this breaks our assumption of the always valid object as there's a place in our code where we create the struct and validate it after creation, although before returning from the constructor.

type Option = func(c *customerOptions)

We could introduce a separate, intermediate struct that is responsible for collecting all the fields that might be influenced by function options. The only issue with this is that we need to maintain the intermediate struct alongside the actual domain object.

type Option = func(c *Customer) error

You might also get an idea to perform validation within the functional option. This violates the Single Responsibility Principle, but you might find some success with it. The biggest issue with this approach is when validation of one field depends on another, you might run into the case in which the order of options starts to become important. This leads to some nasty coupling.

Presets

Presets are slices of functional options that when composed produce a predefined struct. It might make it easier to configure an object for one of the most common use cases. Presets are also helpful when writing tests as they can replace fixtures.

func PremiumMember(args) []Option {
	return []Option{WithLoyaltyPoints(10_000), WithCreditCard(args)}
}

_, _ = NewCustomer("6fa49e0a", "[email protected]", PremiumMember(args)...)

Summary

So what are the downsides? An obvious one is that the Functional Options Pattern requires more code upfront. For Value Objects with just a few fields and which type is unlikely to change like Money or Address, it's simpler to just pass all the required fields as positional arguments. For complex objects the discoverability of options suffers compared to positional arguments, it's also easier to miss something so more attention should be paid to good validation.

I hope this post helps you to better understand the Functional Options Pattern or maybe showed you a different perspective on why it's a good fit for domain objects. By now you should have pretty a good understanding of the pros and cons.

Photo by Croissant on Unsplash.