Question

How to properly convert domain entities to DTOs while considering scalability & testability

I have read several articles and Stackoverflow posts for converting domain objects to DTOs and tried them out in my code. When it comes to testing and scalability I am always facing some issues. I know the following three possible solutions for converting domain objects to DTOs. Most of the time I am using Spring.

Solution 1: Private method in the service layer for converting

The first possible solution is to create a small "helper" method in the service layer code which is convertig the retrieved database object to my DTO object.

@Service
public MyEntityService {

  public SomeDto getEntityById(Long id){
    SomeEntity dbResult = someDao.findById(id);
    SomeDto dtoResult = convert(dbResult);
    // ... more logic happens
    return dtoResult;
  }

  public SomeDto convert(SomeEntity entity){
   //... Object creation and using getter/setter for converting
  }
}

Pros:

  • easy to implement
  • no additional class for convertion needed -> project doesn't blow up with entities

Cons:

  • problems when testing, as new SomeEntity() is used in the privated method and if the object is deeply nested I have to provide a adequate result of my when(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject) to avoid NullPointers if convertion is also dissolving the nested structure

Solution 2: Additional constructor in the DTO for converting domain entity to DTO

My second solution would be to add an additional constructor to my DTO entity to convert the object in the constructor.

public class SomeDto {

 // ... some attributes

 public SomeDto(SomeEntity entity) {
  this.attribute = entity.getAttribute();
  // ... nesting convertion & convertion of lists and arrays
 }

}

Pros:

  • no additional class for converting needed
  • convertion hided in the DTO entity -> service code is smaller

Cons:

  • usage of new SomeDto() in the service code and therefor I have to provide the correct nested object structure as a result of my someDao mocking.

Solution 3: Using Spring's Converter or any other externalized Bean for this converting

If recently saw that Spring is offering a class for converting reasons: Converter<S, T> but this solution stands for every externalized class which is doing the convertion. With this solution I am injecting the converter to my service code and I call it when i want to convert the domain entity to my DTO.

Pros:

  • easy to test as I can mock the result during my test case
  • separation of tasks -> a dedicated class is doing the job

Cons:

  • doesn't "scale" that much as my domain model grows. With a lot of entities I have to create two converters for every new entity (-> converting DTO entitiy and entitiy to DTO)

Do you have more solutions for my problem and how do you handle it? Do you create a new Converter for every new domain object and can "live" with the amount of classes in the project?

Thanks in advance!

 46  40517  46
1 Jan 1970

Solution

 29

Solution 1: Private method in the service layer for converting

I guess Solution 1 will not not work well, because your DTOs are domain-oriented and not service oriented. Thus it will be likely that they are used in different services. So a mapping method does not belong to one service and therefore should not be implemented in one service. How would you re-use the mapping method in another service?

The 1. solution would work well if you use dedicated DTOs per service method. But more about this at the end.

Solution 2: Additional constructor in the DTO for converting domain entity to DTO

In general a good option, because you can see the DTO as an adapter to the entity. In other words: the DTO is another representation of an entity. Such designs often wrap the source object and provide methods that give you another view on the wrapped object.

But a DTO is a data transfer object so it might be serialized sooner or later and send over a network, e.g. using spring's remoting capabilities. In this case the client that receives this DTO must deserialize it and thus needs the entity classes in it's classpath, even if it only uses the DTO's interface.

Solution 3: Using Spring's Converter or any other externalized Bean for this converting

Solution 3 is the solution that I also would prefer. But I would create a Mapper<S,T> interface that is responsible for mapping from source to target and vice versa. E.g.

public interface Mapper<S,T> {
     public T map(S source);
     public S map(T target);
}

The implementation can be done using a mapping framework like modelmapper.


You also said that a converter for each entity

doesn't "scale" that much as my domain model grows. With a lot of entities I have to create two converters for every new entity (-> converting DTO entitiy and entitiy to DTO)

I doupt that you only have to create 2 converter or one mapper for one DTO, because your DTO is domain-oriented.

As soon as you start to use it in another service you will recognize that the other service usually should or can not return all values that the first service does. You will start to implement another mapper or converter for each other service.

This answer would get to long if I start with pros and cons of dedicated or shared DTOs, so I can only ask you to read my blog pros and cons of service layer designs.

EDIT

About the third solution: where do you prefer to put the call for the mapper?

In the layer above the use cases. DTOs are data transfer objects, because they pack data in data structures that are best for the transfer protocol. Thus I call that layer the transport layer. This layer is responsible for mapping use case's request and result objects from and to the transport representation, e.g. json data structures.

EDIT

I see you're ok with passing an entity as a DTO constructor parameter. Would you also be ok with the opposite? I mean, passing a DTO as an Entity constructor parameter?

A good question. The opposite would not be ok for me, because I would then introduce a dependency in the entity to the transport layer. This would mean that a change in the transport layer can impact the entities and I don't want changes in more detailed layers to impact more abstract layers.

If you need to pass data from the transport layer to the entity layer you should apply the dependency inversion principle.

Introduce an interface that will return the data through a set of getters, let the DTO implement it and use this interface in the entities constructor. Keep in mind that this interface belongs to the entity's layer and thus should not have any dependencies to the transport layer.

                                interface
 +-----+  implements     ||   +------------+   uses  +--------+
 | DTO |  ---------------||-> | EntityData |  <----  | Entity |
 +-----+                 ||   +------------+         +--------+
2017-12-17

Solution

 17

I like the third solution from the accepted answer.

Solution 3: Using Spring's Converter or any other externalized Bean for this converting

And I create DtoConverter in this way:

BaseEntity class marker:

public abstract class BaseEntity implements Serializable {
}

AbstractDto class marker:

public class AbstractDto {
}

GenericConverter interface:

public interface GenericConverter<D extends AbstractDto, E extends BaseEntity> {

    E createFrom(D dto);

    D createFrom(E entity);

    E updateEntity(E entity, D dto);

    default List<D> createFromEntities(final Collection<E> entities) {
        return entities.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

    default List<E> createFromDtos(final Collection<D> dtos) {
        return dtos.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

}

CommentConverter interface:

public interface CommentConverter extends GenericConverter<CommentDto, CommentEntity> {
}

CommentConveter class implementation:

@Component
public class CommentConverterImpl implements CommentConverter {

    @Override
    public CommentEntity createFrom(CommentDto dto) {
        CommentEntity entity = new CommentEntity();
        updateEntity(entity, dto);
        return entity;
    }

    @Override
    public CommentDto createFrom(CommentEntity entity) {
        CommentDto dto = new CommentDto();
        if (entity != null) {
            dto.setAuthor(entity.getAuthor());
            dto.setCommentId(entity.getCommentId());
            dto.setCommentData(entity.getCommentData());
            dto.setCommentDate(entity.getCommentDate());
            dto.setNew(entity.getNew());
        }
        return dto;
    }

    @Override
    public CommentEntity updateEntity(CommentEntity entity, CommentDto dto) {
        if (entity != null && dto != null) {
            entity.setCommentData(dto.getCommentData());
            entity.setAuthor(dto.getAuthor());
        }
        return entity;
    }

}
2017-12-20