Boilerplate code is repetitive supporting code required around simple data rather than distinctive application behavior. Encapsulation can protect fields while burying their meaning under constructors, getters, setters, and other maintenance-heavy scaffolding. section 4.1.2
A small class with a large maintenance surface
A two-field Person class can require private fields, multiple constructors, getters, setters, equality methods, hashing, string conversion, and builder machinery. Adding or renaming one field then spreads changes across this supporting code, increasing refactoring risk. A builder improves construction readability, while Lombok can generate much of the implementation. section 4.1.2
Listing 4.1 The Person DTO written with full boilerplate code
public class Person {
private String firstname;
private String lastname;
public Person() {} #1
#1
public Person( #1
String firstname, #1
String lastname) { #1
this.firstname = firstname; #1
this.lastname = lastname; #1
} #1
public void setLastname(String lastname) { #2
this.lastname = lastname; #2
} #2
public void setFirstname(String firstname) { #2
this.firstname = firstname; #2
} #2
public String getFirstname() { #3
return firstname; #3
} #3
public String getLastname() { #3
return lastname; #3
} #3
@Override #4
public String toString() { #4
return "Person{" + #4
"firstname='" + firstname + '\'' + #4
", lastname='" + lastname + '}'; #4
} #4
#4
@Override #4
public boolean equals(Object o) { #4
if (this == o) return true; #4
if (!(o instanceof Person person)) #4
return false; #4
return Objects.equals(firstname, person.firstname) #4
&& Objects.equals( #4
lastname, #4
person.lastname); #4
} #4
#4
@Override #4
public int hashCode() { #4
return Objects.hash(firstname, lastname); #4
} #4
private Person(Builder builder) { #5
this.firstname = builder.firstname; #5
this.lastname = builder.lastname; #5
} #5
#5
public static class Builder { #5
private String firstname; #5
private String lastname; #5
#5
public Builder setFirstname(String firstname) { #5
this.firstname = firstname; #5
return this; #5
} #5
#5
public Builder setLastname(String lastname) { #5
this.lastname = lastname; #5
return this; #5
} #5
#5
public Person build() { #5
return new Person(this); #5
} #5
} #5
} #5
Inspect the verbose Person DTO to see how simple data becomes surrounded by constructors, accessors, and other supporting methods. section 4.1.2
Person p = Person.builder().firstname("John")
.lastname("Doe")
.town("Grenoble")
.build();
Compare the explicit builder calls with positional construction to see how a construction API can make selected fields and intent easier to read. section 4.1.2
Use the following heuristics when supporting code begins to outweigh the data it serves:
- Prefer concise immutable data carriers when the language provides them, including Java records, Kotlin data classes, C# records, or Python dataclasses. section 4.4
- Treat boilerplate as a clarity and maintenance concern, not merely a line-count problem: generic supporting code can obscure intent and become a source of bugs. section 4.1.2

