Overview
Builder is a creational design pattern used to construct complex objects step-by-step with explicit configuration stages. In real backend services, this pattern helps teams keep modules decoupled while still shipping features quickly.
Problem It Solves
Without Builder, teams often mix orchestration, external integration, and domain rules in one place. That leads to high coupling, difficult testing, and expensive refactoring.
5W + 1H
What
Builder is a pattern that structures collaboration between components to solve a recurring design problem.
Why
To increase maintainability, isolate change, and enforce clear boundaries between policy and implementation detail.
Who
Backend engineers, platform engineers, and API teams working with integrations and evolving business logic.
When
Use Builder when your service complexity is growing and one responsibility starts bleeding into unrelated modules.
Where
Apply it in service boundaries such as application use cases, integration adapters, workflow orchestration, and domain policies.
How
Define stable ports first, then implement adapters and wire them through use-case orchestration.
Go Implementation (Step-by-step)
1) Port / interface definitions
package ports
import "context"
type BuilderInput struct {
RequestID string
Payload map[string]any
}
type BuilderOutput struct {
Status string
Data map[string]any
}
type BuilderPort interface {
Execute(ctx context.Context, in BuilderInput) (BuilderOutput, error)
}2) Adapter / implementation
package adapters
import (
"context"
"fmt"
"myapp/internal/ports"
)
type BuilderAdapter struct {
providerName string
}
func NewBuilderAdapter(providerName string) *BuilderAdapter {
return &BuilderAdapter{providerName: providerName}
}
func (a *BuilderAdapter) Execute(ctx context.Context, in ports.BuilderInput) (ports.BuilderOutput, error) {
if in.RequestID == "" {
return ports.BuilderOutput{}, fmt.Errorf("request id is required")
}
return ports.BuilderOutput{
Status: "ok",
Data: map[string]any{
"pattern": "Builder",
"provider": a.providerName,
},
}, nil
}3) Use-case / main wiring
package main
import (
"context"
"fmt"
"log"
"myapp/internal/adapters"
"myapp/internal/ports"
)
func run(ctx context.Context, svc ports.BuilderPort) error {
out, err := svc.Execute(ctx, ports.BuilderInput{
RequestID: "REQ-1001",
Payload: map[string]any{"source": "api"},
})
if err != nil {
return err
}
fmt.Println("status:", out.Status, "pattern:", out.Data["pattern"])
return nil
}
func main() {
ctx := context.Background()
service := adapters.NewBuilderAdapter("default-provider")
if err := run(ctx, service); err != nil {
log.Fatal(err)
}
}Suggested Project Structure
cmd/
api/
main.go
internal/
domain/
builder-pattern-go/
model.go
policy.go
application/
builder-pattern-go/
usecase.go
ports/
builder-pattern-go_port.go
adapters/
inbound/
http/
builder-pattern-go_handler.go
outbound/
builder-pattern-go_adapter.go
infrastructure/
config/
loader.go
persistence/
repository.goClean/Hexagonal Placement
- Domain: keeps pure business rules and entities related to Builder usage.
- Application (Use Case): orchestrates request flow and coordinates domain + ports.
- Ports: defines stable contracts (
BuilderPort) that core logic depends on. - Adapters: implements port behavior (HTTP, gRPC, vendor API, queue consumer).
- Infrastructure: framework-specific and provider-specific setup.
Boundary Rules
- Dependencies point inward: adapters/infrastructure depend on ports/domain, not the reverse.
- Domain must stay framework-agnostic (no HTTP, DB driver, or vendor SDK import).
- Application layer should know interfaces, not concrete adapters.
- Infrastructure may know everything technical, but should not hold business policy.
Real-World Use Case
In production, Builder is useful to compose HTTP client with timeout, retry, circuit breaker, and telemetry. You can keep your use case stable while replacing providers or transport implementations with minimal changes.
Benefits & Tradeoffs
Benefits
- Better modularity and maintainability.
- Easier testability with mocked ports.
- Safer refactoring because boundaries are explicit.
Tradeoffs
- More files and abstractions to maintain.
- Initial learning curve for team members unfamiliar with layered architecture.
Common Pitfalls
- Putting domain rules in adapters.
- Leaking vendor-specific payload directly into domain models.
- Creating too many abstractions without concrete change pressure.
- Skipping contract tests for port behavior.
When NOT to use
- Service is still very small and not expected to grow.
- Team needs a quick prototype and architecture overhead would block delivery.
- There is only one stable integration with no foreseeable variation.
Conclusion
Builder is most effective when paired with Clean/Hexagonal boundaries. The pattern gives structure, while architecture keeps dependencies under control as the codebase evolves.