Builder Pattern Best Practices With Spring Boot
Master the Builder Pattern in Spring Boot for cleaner, more maintainable code
In this article, we’ll explore how to effectively use the Builder Pattern with Spring Boot applications. Many developers use builders incorrectly, leading to bloated constructors and maintenance nightmares. Let’s fix that.
Table of Contents
- Why the Builder Pattern Matters in Spring Boot
- Manual Builder vs Lombok’s @Builder
- Using Builders with JPA Entities and DTOs
- Builder Pattern with Spring Boot Configuration
- Common Mistakes and How to Avoid Them
You can read this article for free by clicking here.
1. Why the Builder Pattern Matters in Spring Boot
When you’re building Spring Boot applications, you’ll inevitably deal with objects that have many fields — DTOs, entities, configuration objects. Using constructors with 6+ parameters is a recipe for bugs. Let’s see the problem:
// Bad: Telescoping constructor anti-pattern
User user = new User("john@example.com", "John", "Doe", "USER", true, LocalDate.now(), null, "New York");
What does that true mean? What about that null? Nobody knows without checking the constructor definition. The Builder Pattern solves this by making object construction readable and flexible.
2. Manual Builder vs Lombok’s @Builder
Let’s start with a manual builder implementation. This is useful when you need custom validation or transformation logic:
public class CreateUserRequest {
private String email;
private String firstName;
private String lastName;
private String role;
private boolean active;
private CreateUserRequest(Builder builder) {
this.email = builder.email;
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.role = builder.role;
this.active = builder.active;
}
public static class Builder {
private String email;
private String firstName;
private String lastName;
private String role = "USER"; // Default value
private boolean active = true; // Default value
public Builder email(String email) {
this.email = email;
return this;
}
public Builder firstName(String firstName) {
this.firstName = firstName;
return this;
}
// ... other builder methods
public CreateUserRequest build() {
validate();
return new CreateUserRequest(this);
}
private void validate() {
if (email == null || email.isBlank()) {
throw new IllegalArgumentException("Email is required");
}
}
}
}
But let’s be honest — writing all that boilerplate is tedious. In most cases, Lombok’s @Builder is the better choice:
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class CreateUserRequest {
private String email;
private String firstName;
private String lastName;
@Builder.Default private String role = "USER";
@Builder.Default private boolean active = true;
}
Now we can use it cleanly:
CreateUserRequest request = CreateUserRequest.builder()
.email("john@example.com")
.firstName("John")
.lastName("Doe")
.build();
Notice how readable this is. Each field name tells you exactly what you’re setting.
3. Using Builders with JPA Entities and DTOs
Here’s where things get interesting. When you have JPA entities and DTOs, builders help you map between them cleanly. Let’s look at a common pattern:
@Entity
@Table(name = "users")
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
private String role;
private boolean active;
private LocalDate createdDate;
}
And the corresponding DTO:
@Data
@Builder
public class UserResponse {
private Long id;
private String email;
private String fullName;
private String role;
private boolean active;
private LocalDate createdDate;
}
Now, how do we convert between them? We can add a static factory method in the builder:
@Data
@Builder
public class UserResponse {
// ... fields
public static UserResponse fromEntity(User user) {
return UserResponse.builder()
.id(user.getId())
.email(user.getEmail())
.fullName(user.getFirstName() + " " + user.getLastName())
.role(user.getRole())
.active(user.isActive())
.createdDate(user.getCreatedDate())
.build();
}
}
Using it in a service:
@Service
public class UserService {
private final UserRepository userRepository;
public UserResponse getUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
return UserResponse.fromEntity(user);
}
}
This pattern keeps your mapping logic in one place and makes it easy to test.
4. Builder Pattern with Spring Boot Configuration
Builders work great with @ConfigurationProperties. Instead of using @Value for every property, define a configuration class with a builder:
@ConfigurationProperties(prefix = "app.notification")
@Builder
@Data
public class NotificationProperties {
@Builder.Default private String host = "localhost";
@Builder.Default private int port = 25;
@Builder.Default private boolean ssl = false;
private String username;
private String password;
@Builder.Default private int retryAttempts = 3;
}
Now in your application.yml:
app:
notification:
host: smtp.example.com
port: 587
ssl: true
username: admin
password: ${SMTP_PASSWORD}
retryAttempts: 5
Wait — you might be thinking, “Spring Boot doesn’t use the builder to set these properties.” You’re right. The builder is useful here when you want to create test configurations or override specific properties programmatically:
NotificationProperties testConfig = NotificationProperties.builder()
.host("localhost")
.port(1025)
.username("test")
.password("test")
.build();
5. Common Mistakes and How to Avoid Them
Mistake 1: Using @Builder on JPA entities without @NoArgsConstructor
Get Hüsna POYRAZ’s stories in your inbox
Join Medium for free to get updates from this writer.
JPA needs a no-arg constructor. Lombok’s @Builder generates an all-args constructor, which breaks JPA. Fix it:
@Entity
@Builder
@NoArgsConstructor // Add this!
@AllArgsConstructor // Required by @Builder
public class User {
// ...
}
Mistake 2: Exposing setters alongside builder
If you use @Data with @Builder, you get both setters and a builder. This can lead to partially built objects floating around. Use @Getter instead:
@Getter
@Builder
public class ImmutableDto {
private final String email;
private final String firstName;
// No setters generated
}
Mistake 3: Forgetting @Builder.Default for primitive defaults
Without @Builder.Default, primitives get Java’s defaults (0, false). Always annotate defaults:
@Builder
public class Config {
@Builder.Default private int timeout = 30; // Without this, timeout = 0
@Builder.Default private boolean enabled = true; // Without this, enabled = false
}
Mistake 4: Using builder for everything
Sometimes a simple constructor is better. If your object has only 2–3 required fields, a constructor is fine:
// Good for 2-3 fields
public record Email(String to, String subject, String body) {}
// Builder needed for 5+ fields with optional parameters
@Builder
public class EmailRequest {
private String to;
private String from;
private String subject;
private String body;
@Builder.Default private boolean html = false;
@Builder.Default private int priority = 3;
private List<String> attachments;
}
Putting It All Together
Here’s a complete example showing builder best practices in a Spring Boot REST controller:
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserResponse> createUser(@RequestBody @Valid CreateUserRequest request) {
UserResponse response = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public UserResponse createUser(CreateUserRequest request) {
User user = User.builder()
.email(request.getEmail())
.firstName(request.getFirstName())
.lastName(request.getLastName())
.role(request.getRole())
.active(true)
.createdDate(LocalDate.now())
.build();
User savedUser = userRepository.save(user);
return UserResponse.fromEntity(savedUser);
}
}
The Builder Pattern isn’t just about avoiding long constructor calls. It’s about making your code self-documenting and reducing the chance of bugs from parameter misordering. Use it when you have objects with 4+ fields, especially when many are optional.
Tags: java spring spring-boot builder-pattern lombok software-engineering software-development design-patterns
References:
To support my work, please follow and clap.










