Skip to content

Latest commit

 

History

History
92 lines (63 loc) · 5.36 KB

File metadata and controls

92 lines (63 loc) · 5.36 KB

Builder — Real-World Applications


1. HTTP Request Construction (this implementation)

Problem: An HTTP request has a URL, method, headers, body, and timeout — most of which are optional. Passing all of them to a constructor produces unreadable call sites, and nothing prevents a caller from producing an invalid combination (e.g., a missing URL or an unsupported method).

How Builder helps: NewRequestBuilder().WithMethod("GET").WithUrl(...).Build() lets callers set only the fields they care about. Build() then validates the assembled state in one place — URL required, method required, method must be a known verb. The private struct ensures every Request a caller holds is guaranteed to have passed validation.

Special case — method normalization: WithMethod calls strings.ToUpper internally. Callers can pass "get", "Get", or "GET" interchangeably — the builder owns the normalization, not the caller.


2. SQL Query Builder

Problem: A query has a table, optional WHERE clauses, ORDER BY, LIMIT, JOIN conditions, and selected columns. Constructing this as a single function call is unreadable; a raw string concatenation is error-prone and unsafe.

How Builder helps: NewQueryBuilder().From("users").Where("age > 18").OrderBy("name").Limit(50).Build() assembles the query step by step. Build() validates that at minimum a table name was provided, then renders the final SQL string. Each method appends to an internal slice — the builder owns the join logic and escaping.

Special case — conditional clauses: Because the builder holds intermediate state, callers can conditionally call methods:

b := NewQueryBuilder().From("orders")
if filterByStatus {
    b = b.Where("status = 'active'")
}
query, err := b.Build()

This is cleaner than building a string with if blocks scattered across call sites.


3. Configuration Object for Service Initialization

Problem: A service (HTTP server, gRPC server, database pool) has dozens of tunables — port, timeouts, TLS settings, max connections, retry policy. Not all are required, defaults are sensible for most, but the ones that are set must be valid.

How Builder helps: NewServerConfig().WithPort(8080).WithTLSCert("cert.pem").WithMaxConns(100).Build() provides named, self-documenting construction. Fields not set retain safe defaults initialized in the constructor. Build() validates combinations — e.g., TLS key must be provided if TLS cert is set.

Why not a config struct literal? A struct literal requires the caller to know all field names and set sensible defaults manually. Builder centralizes defaults and validation, and the private struct prevents callers from bypassing either.


4. Email / Notification Message

Problem: A notification has a recipient, subject, body, optional CC list, optional attachments, and a priority level. Not all fields are always present, but certain combinations are invalid (e.g., empty recipient, both HTML and plain-text body set without a MIME type).

How Builder helps:

msg, err := NewMessageBuilder().
    To("user@example.com").
    Subject("Your order shipped").
    WithHTMLBody("<p>Details...</p>").
    WithAttachment("invoice.pdf").
    Build()

Build() validates recipient presence, resolves MIME type based on body type, and returns a ready-to-send Message interface. The builder owns the logic for what constitutes a valid message.


5. Test Fixture Construction

Problem: Unit tests need to create complex objects (a user, an order, a transaction) with slightly different configurations for each test case. Repeating full struct literals across tests is verbose and brittle when the struct changes.

How Builder helps: A test builder provides sensible defaults and lets each test override only the field relevant to that case:

// Default valid user
user := NewUserBuilder().Build()

// User with expired subscription for billing tests
expiredUser := NewUserBuilder().WithSubscriptionEnd(yesterday).Build()

The builder centralizes the "what makes a valid test user" logic. When the User struct gains a new required field, only the builder's constructor needs updating — all tests continue to work.

Special case — this pattern is sometimes called Object Mother: When the builder is used exclusively in tests and returns pre-configured "canonical" objects (a standard admin user, a standard paid order), it is often called an Object Mother. Builder and Object Mother are the same structural pattern applied to test fixtures rather than production objects.


Summary: When Builder earns its complexity

Application What Builder prevents
HTTP Request Invalid method, missing URL, nil header map panic
SQL Query Raw string concatenation, missing table, unsafe input
Service Config Caller setting contradictory options, missing defaults
Notification Message Empty recipient, conflicting body types
Test Fixtures Repeated boilerplate, brittleness when structs change

The interview insight: Builder is the right choice when the product has enough optional fields that a constructor becomes unreadable, and when validation logically spans multiple fields. Its value is not just ergonomics — it is that Build() is the single point where correctness is asserted, and the private struct guarantees every instance that escapes the package is valid.