Early Access: 87 spots left.

Claim
Low Level DesignBuilderBuilder: Refactor the EmailMessage Constructor

Course content

Builder: Refactor the EmailMessage Constructor

Medium·Tagsdesign-patternsbuilderimmutabilityrefactoring

Problem Statement

The starter `EmailMessage` is constructed through a single 9-parameter public constructor. Call sites pass `null` for optional collections, line up booleans positionally, and become unreadable as soon as a couple of optional flags are involved: ```java // Required: to, from, subject, body. Optional: cc, bcc, attachments, important, readReceipt. new EmailMessage("a@x.com", "b@x.com", "Hi", "Hello", List.of("c@x.com"), null, null, true, false); ``` Which boolean is `important` and which is `readReceiptRequested`? Why is one collection a list and the other two `null`? The constructor signature is the only documentation, and adding a tenth field means breaking every existing caller. Refactor `EmailMessage` into the Builder pattern so the call site becomes self-documenting: ```java EmailMessage email = EmailMessage.builder() .to("a@x.com") .from("b@x.com") .subject("Hi") .body("Hello") .cc(List.of("c@x.com")) .important(true) .build(); ``` After your refactor the design must expose this exact public API: ```java class EmailMessage { public static EmailMessage.Builder builder(); public String getTo(); public String getFrom(); public String getSubject(); public String getBody(); // default "" when not set public List<String> getCc(); // default empty, immutable public List<String> getBcc(); // default empty, immutable public List<String> getAttachments(); // default empty, immutable public boolean isImportant(); // default false public boolean isReadReceiptRequested();// default false // No public constructor. Builder.build() is the only way to construct. public static class Builder { public Builder to(String to); public Builder from(String from); public Builder subject(String subject); public Builder body(String body); public Builder cc(List<String> cc); public Builder bcc(List<String> bcc); public Builder attachments(List<String> attachments); public Builder important(boolean important); public Builder readReceiptRequested(boolean readReceiptRequested); public EmailMessage build(); // throws IllegalStateException when required fields are missing } } ``` The validator runs four bundled checks that cover the design contract: 1. build_and_chain — required-only `builder().to(t).from(f).subject(s).build()` returns an EmailMessage with the given to/from/subject and the documented defaults (body `""`, cc/bcc/attachments empty lists, both booleans `false`); building with every optional field set returns those values; the fluent chain returns the same `Builder` instance throughout (so every setter must be `return this`). 2. missing_required_throws — `build()` without `to` (or `from`, or `subject`) throws `IllegalStateException`. 3. structural — `EmailMessage` declares no public constructor and no public `setXxx` methods. The only construction path is `Builder.build()`; the built object is structurally immutable. 4. immutability_and_defensive_copy — `getCc()`/`getBcc()`/`getAttachments()` return collections that throw `UnsupportedOperationException` on mutation, AND mutating the original list passed to `builder.cc(...)` AFTER `build()` returns does not change the built EmailMessage's cc list (Builder must defensively copy on the way in). The smelly starter has a public 9-arg constructor and a stub `Builder` whose methods all throw `UnsupportedOperationException`, so it fails every behavioural check until the refactor is real.

Examples

Example 1
Input
EmailMessage.builder().to("a@x.com").from("b@x.com").subject("Hi").build()
Output
"EmailMessage{to=a@x.com, from=b@x.com, subject=Hi, body=\"\", cc=[], bcc=[], attachments=[], important=false, readReceiptRequested=false}"
Why
Optional fields fall back to documented defaults. body defaults to empty string; the three lists default to empty immutable lists; both booleans default to false.
Example 2
Input
EmailMessage.builder().to("a").from("b").subject("s").cc(List.of("c", "d")).important(true).build()
Output
"Email with cc=[c, d] and important=true; remaining optionals at defaults."
Why
Each fluent setter overrides one field. The chain is self-documenting at the call site.
Example 3
Input
EmailMessage.builder().from("b").subject("s").build()
Output
"throws IllegalStateException(\"to is required\")"
Why
build() validates required fields. The exception names the missing field so the caller can fix it without reading the implementation.

Constraints

  • EmailMessage must declare zero public constructors. The only way to construct it is EmailMessage.builder().build().
  • EmailMessage.builder() must return an EmailMessage.Builder instance.
  • Every fluent setter on Builder must return the same Builder instance so chaining works.
  • Builder.build() must throw IllegalStateException when any of to, from, or subject is missing.
  • EmailMessage's getCc(), getBcc(), getAttachments() must return immutable collections (mutation throws UnsupportedOperationException).
  • Mutating a list AFTER passing it to builder.cc(...)/bcc(...)/attachments(...) must not change the built EmailMessage's collections — defensive copy is required.
  • EmailMessage must declare no public setter methods (no setXxx). All fields are private final.