Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

Builder — Creational Design Pattern

This is Builder, not Factory. Factory decides which object to create. Builder decides how to construct a single complex object step by step — separating the construction logic from the representation so the same builder can produce different configurations of the same type.


What is it?

The Builder pattern constructs a complex object incrementally through a fluent chain of setter-style calls, deferring final assembly (and validation) to a single Build() call. The caller never touches the struct directly — only the builder's public methods.

It sits in the Creational family because its sole concern is controlled, validated construction of an object whose fields would be error-prone to set all at once.


When to use it

  • When an object has many optional or conditional fields, and passing them all to a constructor would produce an unreadable call site.
  • When construction requires validation across multiple fields before the object is considered valid (e.g., a URL is required, the method must be a known HTTP verb).
  • When you want to keep the underlying struct private so callers can only receive a well-formed instance.

Avoid it when the object is simple enough that a plain struct literal or a short constructor is clear. Builder adds indirection that is only justified when construction is genuinely complex.


Builder vs Factory — side by side

Factory Builder
Goal Choose which type to create Configure how to construct one type
Entry point One function, returns a ready object Returns a builder; caller chains setters, then calls Build()
Validation At selection time (e.g., unknown driver) At Build() time, after all fields are set
Example here NewDatabaseFactory("postgres")DatabaseFactory NewRequestBuilder().WithMethod("GET").WithUrl(...).Build()Request

Go's approach vs. OOP languages

In Java or C++, Builder often involves a nested static Builder class inside the product class. Go's approach is leaner:

Mechanism Purpose
Request interface Exported contract for the built object — callers program against this, never the concrete struct
Unexported request struct The actual product; callers outside the package cannot name or instantiate it directly
RequestBuilder struct Holds the in-progress request and exposes setter methods, each returning *RequestBuilder for chaining
Build() (Request, error) The terminal step — validates the assembled state and returns the interface or an error

Because the struct is unexported and all fields are lowercase, the only way to produce a Request is through the builder. The compiler enforces this.


Structure of this implementation

builder/
├── main.go                                        # Usage demonstration
└── request_builder/
    ├── request.go                                 # Request interface + private request struct + getters
    ├── request_builder.go                         # RequestBuilder — fluent setter chain + Build()
    └── request_builder_test.go                    # Construction and nil-safety tests

The product interface

request_builder/request.go declares the Request interface:

type Request interface {
    GetTimeout() int
    GetUrl() string
    GetMethod() string
    GetBody() string
    GetHeaders() map[string]string
}

The concrete request struct (lowercase) satisfies this interface via value-receiver getter methods. Value receivers are deliberate — getters do not mutate state, and value receivers ensure both request and *request satisfy the interface, so Build() can return a value without a pointer.

The builder

request_builder/request_builder.go holds the construction logic:

Method Behavior
NewRequestBuilder() Initializes the builder with a headers map pre-allocated (avoids nil map panic in WithHeader)
WithUrl(url) Sets the URL field
WithMethod(method) Sets the method, normalized to uppercase via strings.ToUpper
WithTimeout(timeout) Sets the timeout
WithBody(body) Sets the request body
WithHeader(key, val) Adds a single header to the pre-initialized map
WithHeaders(map) Replaces the headers map; nil input is converted to an empty map
Build() Validates and returns (Request, error)

Validation in Build()

Build() enforces three rules before returning:

func (rb *RequestBuilder) Build() (Request, error) {
    // 1. URL must not be empty
    // 2. Method must not be empty
    // 3. Method must be one of: GET, POST, DELETE, PATCH
}

Validation lives in Build() rather than in individual setters so partial builders remain valid mid-chain. A builder with only a URL set is fine until Build() is called.


Why the concrete struct is unexported

If request were exported, callers could write requestbuilder.request{Method: "INVALID"} and bypass all validation. Keeping the struct unexported means Build() is the only exit point — every Request a caller ever holds is guaranteed to be valid.


Testing

TestRequestBuilderNilCondition verifies:

Scenario What is verified
Empty body Build succeeds without panic
WithHeaders(nil) Nil map is safely replaced with an empty map
WithHeader after WithHeaders(nil) No nil map panic — the replacement guard makes this safe

Additional test cases that would complete coverage:

  • Build() with empty URL → expect non-nil error
  • Build() with empty method → expect non-nil error
  • Build() with unsupported method (e.g. "PUT") → expect non-nil error
  • Build() with valid method and URL → expect non-nil Request, nil error
  • WithMethod with lowercase input → verify it is normalized to uppercase

Key interview talking points

Q: Why does Build() return an interface rather than the concrete struct? The concrete struct is unexported. Returning an interface is the only way to give external callers a typed handle. It also enforces the contract: callers interact with Request, never with internal field layout.

Q: Why are the getter methods value receivers, not pointer receivers? Getters do not mutate state, so a value receiver is the correct semantic. More critically, a value receiver means the concrete request type (not just *request) satisfies the Request interface. Since Build() returns a value (rb.requestInProgress), pointer receivers would cause a compile error — the value would not satisfy the interface.

Q: Why is validation deferred to Build() instead of individual setters? Individual setters fire in any order. A URL-only builder is legitimately incomplete mid-chain. Deferring validation to Build() means the builder can hold an invalid partial state safely — it is only at assembly time that the rules are enforced.

Q: What prevents a caller from mutating the returned Request? Two things: the struct is unexported (so callers cannot cast to it), and the Request interface exposes only getters. There is no setter surface on the interface — once built, the request is effectively immutable from the caller's perspective.

Q: Why does NewRequestBuilder() pre-allocate the headers map? Writing to a nil map in Go panics at runtime. WithHeader does a direct map assignment. If the map were lazily initialized (only when WithHeader is called), WithHeaders(nil) followed by WithHeader would panic. Pre-allocating in the constructor makes the zero-state safe regardless of call order.