Of course! The term "Java Bean to Bean" mapping is a very common task in Java development. It refers to the process of copying data from one object (the source bean) to another object (the destination bean) when they have similar but not necessarily identical structures.

Let's break down the concept, explore the manual way, and then dive into the best practices using popular libraries.
Why Do We Need Bean-to-Bean Mapping?
In real-world applications, you often work with different representations of the same data:
- Separation of Concerns: Your database entity (e.g.,
JpaUser) should be separate from your data transfer object (DTO) for a REST API (e.g.,UserDto). This prevents your database schema from directly exposing your API structure and vice-versa. - Security: You might want to expose only a subset of an entity's fields in a DTO (e.g., not the password hash).
- API Stability: You can change your internal database model without breaking your public API by keeping the DTO stable.
- Different Data Shapes: The data needed for a form might be different from the data needed to display a summary.
Scenario
Let's use a consistent example throughout. We have a User entity from a database and a UserDto to be sent to a client.
Source Bean: User.java (Entity)

import java.time.LocalDateTime;
public class User {
private Long id;
private String username;
private String passwordHash; // We don't want to expose this
private String email;
private LocalDateTime createdAt;
// Constructors, Getters, and Setters (omitted for brevity, but essential for a JavaBean)
public User() {}
public User(Long id, String username, String passwordHash, String email, LocalDateTime createdAt) {
this.id = id;
this.username = username;
this.passwordHash = passwordHash;
this.email = email;
this.createdAt = createdAt;
}
// Getters and Setters...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
Destination Bean: UserDto.java (Data Transfer Object)
// We'll use a String for the date for simplicity in JSON serialization
public class UserDto {
private Long id;
private String username;
private String email;
private String creationDate; // Different field name and type
// Constructors, Getters, and Setters...
public UserDto() {}
public UserDto(Long id, String username, String email, String creationDate) {
this.id = id;
this.username = username;
this.email = email;
this.creationDate = creationDate;
}
// Getters and Setters...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getCreationDate() { return creationDate; }
public void setCreationDate(String creationDate) { this.creationDate = creationDate; }
}
Method 1: Manual Mapping (The "Old School" Way)
This involves writing the mapping logic by hand. It's straightforward for one-off, simple mappings.
Code:
public class ManualMapper {
public UserDto mapToDto(User user) {
if (user == null) {
return null;
}
UserDto dto = new UserDto();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
dto.setEmail(user.getEmail());
// Type conversion and renaming logic
if (user.getCreatedAt() != null) {
dto.setCreationDate(user.getCreatedAt().toString());
}
return dto;
}
public User mapToEntity(UserDto dto) {
if (dto == null) {
return null;
}
User user = new User();
user.setId(dto.getId());
user.setUsername(dto.getUsername());
user.setEmail(dto.getEmail());
// Note: passwordHash would need to be set separately, e.g., during registration.
return user;
}
}
Pros:
- No Dependencies: No external libraries needed.
- Full Control: You have complete control over the mapping logic.
Cons:
- Verbose & Repetitive: A lot of boilerplate code.
- Error-Prone: It's easy to forget to map a new field.
- Hard to Maintain: As the number of beans grows, the number of mapper classes explodes. This is known as the "Mapper Explosion" problem.
Method 2: Using Mapping Libraries (The Modern Approach)
This is the recommended approach for any non-trivial application. These libraries automate the process, reducing boilerplate and errors.
A. MapStruct
MapStruct is a code generator that creates type-safe and performant bean mappers at compile time. This is often the top choice for production applications.
How it works: You define a mapper interface with @Mapping annotations. During the build, MapStruct generates an implementation class for you.
Setup (Maven):
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version> <!-- Use the latest version -->
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
Code:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper // MapStruct will generate an implementation for this interface
public interface UserMapper {
// Singleton instance of the mapper
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
// Define the mapping for the field with a different name
@Mapping(target = "creationDate", source = "createdAt")
// We can ignore a field from the source
@Mapping(target = "passwordHash", ignore = true)
UserDto userToUserDto(User user);
// The reverse mapping is often possible too
@Mapping(target = "createdAt", source = "creationDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(target = "passwordHash", ignore = true) // Or default it
User userDtoToUser(UserDto userDto);
}
Usage:
User user = new User(1L, "john_doe", "a-very-secure-hash", "john.doe@example.com", LocalDateTime.now());
// Use the generated mapper instance
UserDto userDto = UserMapper.INSTANCE.userToUserDto(user);
System.out.println(userDto);
// Output: UserDto{id=1, username='john_doe', email='john.doe@example.com', creationDate='...'}
Pros:
- Type-Safe: Errors are caught at compile-time.
- High Performance: No reflection; it's just plain Java method calls.
- Readability: The mapping definition is clean and declarative in an interface.
- IDE Support: Excellent autocompletion and refactoring support.
Cons:
- Build-time Dependency: Requires an annotation processor (e.g., in Maven or Gradle).
B. ModelMapper
ModelMapper is a runtime library that uses reflection to map objects. It's very flexible and easy to set up.
Setup (Maven):
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.0</version> <!-- Use the latest version -->
</dependency>
Code:
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
public class ModelMapperExample {
public static void main(String[] args) {
ModelMapper modelMapper = new ModelMapper();
// Set a stricter matching strategy to avoid unintended mappings
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
User user = new User(1L, "jane_doe", "another-secure-hash", "jane.doe@example.com", LocalDateTime.now());
// Perform the mapping
UserDto userDto = modelMapper.map(user, UserDto.class);
System.out.println(userDto);
// Output: UserDto{id=1, username='jane_doe', email='jane.doe@example.com', creationDate='...'}
}
}
For more complex mappings (like field renaming), you can configure the mapper:
// To handle field renaming
modelMapper.typeMap(User.class, UserDto.class)
.addMapping(User::getCreatedAt, UserDto::setCreationDate);
Pros:
- Easy Setup: No build-time plugins needed.
- Flexible: Handles complex nested mappings and custom logic well.
- Convention-based: Can automatically map fields with the same name.
Cons:
- Runtime Performance: Slower than MapStruct due to reflection.
- Less Safe: Errors can only be caught at runtime.
- Debugging: Can be harder to debug as the mapping logic happens at runtime.
Summary and Recommendation
| Feature | Manual Mapping | MapStruct (Compile-Time) | ModelMapper (Runtime) |
|---|---|---|---|
| Performance | Excellent | Excellent (Best) | Good (but slower) |
| Type Safety | High (if you write tests) | Excellent (Compile-time) | Low (Runtime errors) |
| Boilerplate | Very High | Very Low | Low |
| Setup | None | Requires Annotation Processor | Simple Dependency |
| Complexity | Low for simple, high for complex | Low to Medium | Medium |
| Debugging | Easy | Easy (generated code) | Harder |
Final Recommendation:
- For new projects, especially Spring Boot applications, use MapStruct. It provides the best combination of performance, safety, and maintainability. It's the industry standard for high-performance applications.
- For small projects, prototypes, or situations where adding a build-time processor is difficult, ModelMapper is a good, flexible alternative.
- Avoid manual mapping for anything beyond the simplest of cases or when you have only a couple of beans to map. The maintenance cost is not worth it.
