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.