Builder Pattern

The builder pattern 1 allows the client to mutate the data basically at any time – even after the creation of a type. The pattern is widely used in real-life software applications to construct complex types step by step. We can see it in loggers or SQL query builders 2. We can create a basic logger and after that create new ones from the basic one with additional modifications.

type Logger struct {
	level string
	output io.Writer
}

func (l *Logger) WithLevel(level string) *Logger {
	l.level = level
	return l
}

func (l *Logger) WithOutput(w io.Writer) *Logger {
	l.output = w
	return l
}
func NewLogger() *Logger {
	return &Logger{}
}

_ = NewLogger().WithLevel("one").WithOutput(os.Stdout)

The benefits of using this pattern are:

  • Handles optional parameters elegantly: The builder pattern is idiomatic solution for the lack of default function arguments. Callers specify only what they care about and everything else is set to sensible defaults.
  • Improves readability: Instead of functions with multiple parameters, we have functions with self-descriptive names.
  • Safe construction: The type is not ready to be used until it’s fully constructed. The client decides when the type meets all the requirement of configuration.
  • Backwards compatibility: We can extend the type with new options without breaking existing callers. This is a big deal in library design, where you can’t break downstream consumers.
  • Enforces validation: Validation of values are placed in the methods.
  • Testability: Tests can construct objects with only the fields relevant to the test case, keeping test setup minimal and intent clear.

The Functional Options variant adds one more benefit: each option is a plain function, meaning options themselves can be composed, stored, passed around, and reused as values — treating configuration as first-class citizens.

Copyright © 2026 by Michal Przybylowicz