Modern enterprise Java applications demand clean architecture, immutability, and maintainable code. As software engineers working in companies, we often rely on Spring Boot, Spring Data JPA, and Lombok to build scalable backend systems. One Lombok feature that is widely used—but often misused—is the @Builder pattern.
Incorrect usage of Lombok Builder JPA can cause persistence issues, broken Hibernate proxies, unexpected runtime errors, and long-term maintenance problems. This article provides a production-ready, step-by-step solution that demonstrates how to correctly use Lombok’s @Builder with Spring Data JPA while ensuring Immutable DTOs in Java and following Code Generation Best Practices.
Why Immutability Matters in Enterprise Applications
Problems with Mutable Objects
- Unexpected side effects
- Thread-safety issues
- Hidden bugs in concurrent systems
- Difficult debugging in production
Benefits of Immutability
- Thread safety
- Predictable behavior
- Easier unit testing
- Safe caching
- Cleaner architecture
Common Mistakes with Lombok @Builder and JPA
Using @Builder Directly on JPA Entities
@Entity
@Builder
@Data
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
}
Why this is wrong:
- JPA requires a no-argument constructor
- Hibernate uses proxies
- @Data exposes setters
- Builder bypasses JPA lifecycle
Correct Industry-Standard Strategy
- JPA Entities → Mutable (Controlled)
- DTOs → Fully Immutable
- @Builder → DTO Layer Only
Project Architecture
com.company.demo
│
├── entity
│ └── UserEntity.java
├── dto
│ └── UserDTO.java
├── repository
│ └── UserRepository.java
├── service
│ └── UserService.java
├── controller
│ └── UserController.java
└── DemoApplication.java
Step 1: Maven Configuration
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.company</groupId>
<artifactId>lombok-builder-jpa-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
Step 2: JPA Entity (No Builder)
UserEntity.java
package com.company.demo.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
protected UserEntity() {
}
public UserEntity(String name, String email) {
this.name = name;
this.email = email;
}
public void updateName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
Step 3: Immutable DTO Using Lombok @Builder
UserDTO.java
package com.company.demo.dto;
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class UserDTO {
Long id;
String name;
String email;
}
This is a perfect example of Immutable DTOs Java.
Fields are final, no setters exist, and objects are created cleanly using the builder.
Step 4: Repository Layer
package com.company.demo.repository;
import com.company.demo.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<UserEntity, Long> {
}
Step 5: Service Layer (Entity → DTO Mapping)
package com.company.demo.service;
import com.company.demo.dto.UserDTO;
import com.company.demo.entity.UserEntity;
import com.company.demo.repository.UserRepository;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
public UserDTO createUser(String name, String email) {
UserEntity entity = new UserEntity(name, email);
UserEntity saved = repository.save(entity);
return UserDTO.builder()
.id(saved.getId())
.name(saved.getName())
.email(saved.getEmail())
.build();
}
public UserDTO getUser(Long id) {
UserEntity entity = repository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
return UserDTO.builder()
.id(entity.getId())
.name(entity.getName())
.email(entity.getEmail())
.build();
}
}
Step 6: REST Controller
package com.company.demo.controller;
import com.company.demo.dto.UserDTO;
import com.company.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService service;
public UserController(UserService service) {
this.service = service;
}
@PostMapping
public UserDTO createUser(@RequestParam String name,
@RequestParam String email) {
return service.createUser(name, email);
}
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return service.getUser(id);
}
}
Step 7: Application Entry Point
package com.company.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
Key Takeaways
- Never use @Builder on JPA entities
- Use @Builder for DTOs only
- Entities should be mutable but controlled
- DTOs must be immutable
- Follow Code Generation Best Practices
Conclusion
Using Lombok’s @Builder correctly with Spring Data JPA is essential for building clean, scalable, and production-ready enterprise applications. By separating mutable entities from immutable DTOs, software engineers can avoid common pitfalls and deliver high-quality systems that stand the test of time.
This approach is widely adopted in real-world company projects and is fully ready for use.
