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("https://github.com/KodeLoad/DesignPatternsInGo/tree/mainline/creational/...").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("https://github.com/KodeLoad/DesignPatternsInGo/tree/mainline/creational/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.