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.
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 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.
| 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 |
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.
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
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.
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) |
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.
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.
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 errorBuild()with empty method → expect non-nil errorBuild()with unsupported method (e.g."PUT") → expect non-nil errorBuild()with valid method and URL → expect non-nilRequest, nil errorWithMethodwith lowercase input → verify it is normalized to uppercase
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.