Builder — Find the Bug¶
12 buggy snippets across Go, Java, Python.
Bug 1: Setter Returns Product Instead of Builder (Java)¶
Symptoms: Compile error: Builder expected, got Product. Or, with type erasure / casting, runtime error.
Find the bug
Setter returns the Product instead of `this`. Chain breaks immediately.Fix¶
Lesson¶
Every setter returns the Builder. Last step (build()) returns the Product.
Bug 2: Builder Doesn't Copy Mutable Collections (Java)¶
public final class Email {
public final List<String> cc;
private Email(Builder b) { this.cc = b.cc; } // BUG: same reference
public static class Builder {
private final List<String> cc = new ArrayList<>();
public Builder cc(String addr) { cc.add(addr); return this; }
public Email build() { return new Email(this); }
}
}
Builder b = new Email.Builder().cc("a@b.c");
Email e = b.build();
b.cc("d@e.f"); // BUG: mutates email's cc too!
Symptoms: "Frozen" Email's CC list grows when builder is reused.
Find the bug
The Email shares the same `List` reference with the Builder. Mutating either mutates both.Fix¶
Lesson¶
Defensive copy mutable inputs in build() (or in the constructor). Use immutable collections.
Bug 3: Required Field Not Validated (Java)¶
Symptoms: Email.builder().build() succeeds with null to: and null subject. Crashes downstream when fields are accessed.
Find the bug
`build()` doesn't enforce required fields. Errors surface late, far from the construction site.Fix¶
public Email build() {
if (sender == null) throw new IllegalStateException("sender required");
if (to.isEmpty()) throw new IllegalStateException("to required");
if (subject == null) throw new IllegalStateException("subject required");
return new Email(this);
}
Lesson¶
Validate in build(). Fail fast and loudly.
Bug 4: Builder State Leaks Across Builds (Java)¶
public class CarBuilder {
private Car car = new Car();
public CarBuilder seats(int n) { car.seats = n; return this; }
public Car build() { return car; } // BUG: returns the same car
}
CarBuilder b = new CarBuilder();
Car c1 = b.seats(2).build();
Car c2 = b.seats(7).build(); // BUG: c1.seats == 7 too!
Symptoms: c1 and c2 are the same object. Setting seats(7) on the second build affects both.
Find the bug
Builder doesn't reset between builds. Both `build()` calls return the same `Car` reference. The second `seats()` mutates `c1`'s state.Fix — Reset¶
Fix — Construct fresh¶
private final List<Consumer<Car>> setters = new ArrayList<>();
public CarBuilder seats(int n) { setters.add(c -> c.seats = n); return this; }
public Car build() {
Car c = new Car();
setters.forEach(s -> s.accept(c));
return c;
}
Lesson¶
Builder reuse semantics matter. Either reset state or document one-shot use.
Bug 5: Functional Option Captures Wrong Variable (Go)¶
opts := []Option{}
for i := 0; i < 3; i++ {
opts = append(opts, func(s *Server) { s.id = i }) // BUG (Go pre-1.22)
}
s := New(opts...) // s.id = 3, not 0/1/2
Symptoms: All servers have id == 3.
Find the bug
Pre-Go 1.22, the `i` variable is shared across iterations. All closures capture the *same* `i`, which is `3` after the loop. Go 1.22+ fixes this with per-iteration variables, but legacy code still bites.Fix¶
for i := 0; i < 3; i++ {
i := i // shadow with local
opts = append(opts, func(s *Server) { s.id = i })
}
Lesson¶
In Go pre-1.22, range/loop variables in closures are common gotcha. Shadow with local copy.
Bug 6: Director Forgot to Reset Builder (Java)¶
class Director {
public void buildSportsCar(CarBuilder b) {
// BUG: no reset
b.setSeats(2); b.setEngine("V8"); b.setSpoiler(true);
}
}
Director d = new Director();
CarBuilder b = new CarBuilder();
d.buildSportsCar(b); // sets seats, engine, spoiler
Car sportsCar = b.build(); // OK
d.buildSUV(b); // sets seats=7, engine=V6 — but spoiler still true!
Car suv = b.build(); // SUV has a spoiler!
Symptoms: Second build inherits state from first.
Find the bug
Director doesn't reset Builder before configuring. Old state persists.Fix¶
public void buildSportsCar(CarBuilder b) {
b.reset(); // ensure clean slate
b.setSeats(2); b.setEngine("V8"); b.setSpoiler(true);
}
public void buildSUV(CarBuilder b) {
b.reset();
b.setSeats(7); b.setEngine("V6"); b.setAWD(true);
}
Lesson¶
When sharing a Builder across calls, always reset.
Bug 7: Step Builder Type-State Mistake (Java)¶
public interface UrlStep { MethodStep url(String u); }
public interface MethodStep { OptionalStep method(String m); }
public interface OptionalStep { Email build(); }
public class BuilderImpl implements UrlStep, MethodStep, OptionalStep {
public MethodStep url(String u) { return this; }
public OptionalStep method(String m) { return this; }
public Email build() { return new Email(...); } // BUG: no validation
}
// All callers can do:
UrlStep step = builder();
((BuilderImpl) step).build(); // BUG: bypassing the type-state by casting
Symptoms: Type-state is bypassable via casts. Required fields can be skipped.
Find the bug
The `BuilderImpl` is publicly accessible and implements all interfaces. Casting allows skipping steps.Fix¶
private static class BuilderImpl implements UrlStep, MethodStep, OptionalStep { ... }
public static UrlStep builder() { return new BuilderImpl(); }
Make the impl private. Callers can't cast to it. Step ordering is enforced.
Lesson¶
Step Builder requires hiding the implementation class. Otherwise the type system lies.
Bug 8: Python __init__ Re-runs After __new__ (Python)¶
class Builder:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self._url = None # BUG: resets on every Builder() call
self._method = "GET"
Symptoms: Reusing Builder() resets all state.
Find the bug
`__init__` runs every time `Builder()` is called, even when `__new__` returns existing instance. Reinitializes the singleton-like Builder.Fix¶
def __init__(self):
if not hasattr(self, "_initialized"):
self._url = None; self._method = "GET"
self._initialized = True
But really: don't make Builders singletons. They're per-construction.
Lesson¶
__new__ + __init__ interaction is subtle. Builders should be plain instances.
Bug 9: Builder Validates But Builder Returns Anyway (Java)¶
public Email build() {
if (sender == null) System.err.println("sender required"); // BUG: log instead of throw
return new Email(this);
}
Symptoms: Invalid Email returned with null sender. Errors deferred to runtime.
Find the bug
Validation logs but doesn't fail. Construction proceeds with invalid state.Fix¶
Lesson¶
Validation must fail. A logged warning isn't a guarantee.
Bug 10: Mutating Product After Build (Python)¶
@dataclass # BUG: not frozen
class HttpRequest:
url: str
headers: dict[str, str] = field(default_factory=dict)
class HttpRequestBuilder:
def __init__(self): self._url = None; self._headers = {}
def url(self, u): self._url = u; return self
def header(self, k, v): self._headers[k] = v; return self
def build(self):
return HttpRequest(self._url, self._headers) # BUG: shares _headers
req = HttpRequestBuilder().url("/x").header("X", "Y").build()
req.headers["Z"] = "Q" # BUG: mutates! also mutates builder's _headers
Symptoms: Mutations on built object propagate to the builder. Frozen Product not frozen.
Find the bug
Two issues: 1. `@dataclass` (not frozen) lets `req.headers["Z"]` work. 2. `req.headers` and `_headers` are the same dict object.Fix¶
@dataclass(frozen=True)
class HttpRequest:
url: str
headers: dict[str, str] = field(default_factory=dict)
def build(self):
return HttpRequest(self._url, dict(self._headers)) # copy
Lesson¶
Make Product immutable. Defensive copy mutable inputs.
Bug 11: Functional Option With Side Effect (Go)¶
func WithLogger(log *Logger) Option {
log.Info("registered logger") // BUG: side effect at option creation
return func(s *Server) { s.log = log }
}
// Caller
opts := []Option{WithLogger(myLogger)}
// Logger.Info has already fired even before New() is called
Symptoms: Side effects happen before Server construction. If WithLogger is called twice, the log fires twice.
Find the bug
The side effect is in `WithLogger` itself, not inside the returned closure. It runs as soon as the option is created.Fix¶
func WithLogger(log *Logger) Option {
return func(s *Server) {
log.Info("registered logger") // inside the closure
s.log = log
}
}
Lesson¶
Options should be pure factories — side effects belong inside the returned function, not in the option-creating function.
Bug 12: Builder Inherits Wrong Way (Java)¶
public class HttpRequestBuilder {
public HttpRequestBuilder url(String u) { ... return this; }
}
public class GetRequestBuilder extends HttpRequestBuilder {
public GetRequestBuilder cache(boolean c) { ... return this; }
}
GetRequestBuilder b = new GetRequestBuilder().url("/x").cache(true); // compile error
Symptoms: url() returns HttpRequestBuilder (parent), losing access to GetRequestBuilder's cache().
Find the bug
Setter return type isn't covariant. After `.url(...)`, the chain returns `HttpRequestBuilder`, not `GetRequestBuilder`.Fix — Recursive generics (curiously recurring template pattern)¶
public class HttpRequestBuilder<B extends HttpRequestBuilder<B>> {
@SuppressWarnings("unchecked")
public B url(String u) { ... return (B) this; }
}
public class GetRequestBuilder extends HttpRequestBuilder<GetRequestBuilder> {
public GetRequestBuilder cache(boolean c) { ... return this; }
}
GetRequestBuilder b = new GetRequestBuilder().url("/x").cache(true); // OK
Lesson¶
Builder inheritance + fluent chaining requires recursive generics or covariant returns. Lombok @SuperBuilder handles this.
Practice Tips¶
- Run
go test -racefor Go Builder code. - Use
assertImmutabletest helpers for Java records / frozen dataclasses. - Test Builder reuse explicitly:
b.build()twice with different state. - Test that mutating the input collection doesn't affect the built object.
← Tasks · Creational · Roadmap · Next: Optimize