Bean copying looks harmless until it hides a production bug.
I have used BeanUtils.copyProperties() for DTO conversion because it is fast to write. Then later someone adds a nested object, a field name changes, or performance matters, and the convenient helper suddenly becomes a quiet source of confusion.
So here is the practical rule:
Use BeanUtils for simple, boring object copying. Do not use it as your default domain mapping strategy.
The Basic Copy
With Apache Commons BeanUtils:
import org.apache.commons.beanutils.BeanUtils;
public class CopyDemo {
public static void main(String[] args) throws Exception {
UserEntity entity = new UserEntity("Mark", 30);
UserDTO dto = new UserDTO();
BeanUtils.copyProperties(dto, entity);
System.out.println(dto);
}
}
That is the reason people like it. One line replaces a bunch of setters.
But the hidden question is:
What exactly did it copy?
Shallow Copy Is the First Trap
If the object contains nested objects, BeanUtils copies the reference. It does not magically deep-copy your object graph.
class UserEntity {
private String name;
private Address address;
}
class UserDTO {
private String name;
private Address address;
}
After copying, both objects can point at the same Address.
That may be fine. It may also be a bug if one layer mutates the nested object.
Visual map:
UserEntity.address ----+
+----> same Address object
UserDTO.address -------+
If you need independent nested objects, write that mapping explicitly or use a mapper designed for it.
Reflection Has a Cost
BeanUtils usually relies on reflection and runtime property inspection.
That means:
- less compile-time safety
- slower mapping than direct code
- mistakes may show up later
- refactoring fields can be riskier
For one admin endpoint, who cares.
For mapping thousands of rows in a hot path, I would care.
Spring BeanUtils vs Apache BeanUtils
The method names look similar, but behavior differs.
| Tool | Good for | Watch out |
|---|---|---|
Spring BeanUtils
|
simple same-name copy | limited conversion |
Apache BeanUtils
|
string conversion and dynamic property access | reflection overhead |
| MapStruct | production DTO mapping | setup and annotations |
| Manual mapping | critical domain logic | more code |
My current preference:
- quick internal tools: BeanUtils
- API DTO mapping: MapStruct or manual mapping
- sensitive business logic: manual mapping
A Safer DTO Mapping Example
Sometimes boring explicit code is better:
public UserDTO toDto(UserEntity entity) {
UserDTO dto = new UserDTO();
dto.setName(entity.getName());
dto.setAge(entity.getAge());
if (entity.getAddress() != null) {
AddressDTO address = new AddressDTO();
address.setCity(entity.getAddress().getCity());
address.setCountry(entity.getAddress().getCountry());
dto.setAddress(address);
}
return dto;
}
Yes, it is longer.
But during a code review, nobody has to guess what happens to address.
Where BeanUtils Still Makes Sense
I would still use it for:
- small admin tools
- test fixtures
- simple JavaBean-to-JavaBean copying
- prototypes
- non-critical internal scripts
I would avoid it for:
- hot paths
- payment/order domain mapping
- complex nested DTOs
- security-sensitive field filtering
- APIs where fields change often
Production Pitfall: Copying Too Much
This bug is easy to miss:
BeanUtils.copyProperties(userEntity, requestDto);
If requestDto contains fields like role, status, or createdAt, you may accidentally let user input overwrite fields it should not control.
For request-to-entity updates, I prefer explicit allow-list mapping:
user.setDisplayName(request.getDisplayName());
user.setBio(request.getBio());
It is less clever. It is safer.
Final Thought
BeanUtils is a convenience tool, not an architecture.
Use it where the mapping is boring. When the mapping carries business meaning, write it so the next developer can see every decision.
Have you ever had a DTO copy bug caused by a field you did not mean to map?
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.