Data Transfer Object Pattern in Java - Implementation and Mapping

Introduction

An enterprise application is a software solution created for the needs of an organization. It's oftentimes a large-scale, multi-tiered, scalable system. Enterprise software can deal with a lot of complex data and it's important for this type of software to have good architecture.

Enterprise application architecture patterns are standardized solutions to common problems found in large systems. They cultivate architectural thinking and help developers be more confident in building systems with proven reliability.

Enterprise applications can be charged with manipulating, displaying or storing huge amounts of data. Avoiding tight coupling and ensuring data integrity/security mustn't be an afterthought when working on these applications.

Data Transfer Object

The Data Transfer Object Design Pattern is one of the enterprise application architecture patterns that calls for the use of objects that aggregate and encapsulate data for transfer. A Data Transfer Object is, essentially, like a data structure. It should not contain any business logic but should contain serialization and deserialization mechanisms.

DTOs can either contain all the data from a source, or partial data. They can hold data from single or multiple sources as well. When implemented, DTOs become the means of data transport between systems.

Martin Fowler describes the Data Transfer Object in his famous book Patterns of Enterprise Application Architecture. There, the main idea of DTOs is to reduce the number of remote calls that are expensive.

Martin Fowler also defines an assembler object, used to convert data between the DTO and any entity objects. Nowadays, we use mappers for that purpose.

What's worth noting is that applying the Data Transfer Object pattern can become an anti-pattern in local systems. It's meant to be used in remote calls to promote security and loose coupling. If applied to local systems, it's just over-designing a simple feature.

Motivation

Let's assume that we have to develop an enterprise system for a company. The system will include a database with various general information about employees - salary, projects, certificates, personal data (address, family status, phone number, etc.).

The security at the entrance of the company requires access to our system, to identify the worker that wants to enter. They need some rudimentary information, such as the surname and photo of the worker.

We don't want to send other sensitive information to the security system, such as personal information. It's redundant and exposes the communication channel between the systems to attacks. We'll only provide what needed, and the scope of data will be defined in a DTO.

In Java applications - we use entity classes to represent tables in a relational database. Without DTOs, we'd have to expose the entire entities to a remote interface. This causes a strong coupling between an API and a persistence model.

By using a DTO to transfer just the required information, we loosen the coupling between the API and our model, allowing us to more easily maintain and scale the service.

Implementing a Data Transfer Object

Let's make an application that takes care of location tracking for your friends. We'll build a Spring Boot application that exposes a REST API. Using it, we'll be able to retrieve user locations from an H2 database.

If you'd like to read on Integrating an H2 Database with Spring Boot, we've got you covered!

Setting up Spring Boot

The easiest way to start off with a blank Spring Boot app is to use Spring Initializr:

Alternatively, you can also use the Spring Boot CLI to bootstrap the application:

$ spring init --dependencies=h2 data-transfer-object-demo

If you already have a Maven/Spring application, add the dependency to your pom.xml file:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>${version}</version>
</dependency>

Or if you're using Gradle:

compile group: 'com.h2database', name: 'h2', version: '${version}'

Demo Application

Let's start off with the User model:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String firstName;
    private String lastName;
    private String password;
    private String email;
        
    @ManyToOne(fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = "location_id")
    private Location location;
        
    // Getters and Setters
}

It contains some rudimentary information like the username, firstName, email, etc. It also has a many-to-one relationship with the Location entity:

@Entity
public class Location {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private double lat;
    private double lng;
    private String place;
    private String description;
    
        // Getters and Setters
}

For basic CRUD operations, we'll rely on the trusty CrudRepository provided by Spring Boot:

@Repository
public interface UserRepository extends CrudRepository<User, Long>{}
@Repository
public interface LocationRepository extends CrudRepository<Location, Long> {}

If you're unsure how these work, we suggest reading our Guide to Spring Data JPA. In short, they'll bootstrap us with basic CRUD functionality for our models.

At this point, we'd want to make a controller that handles a GET request and returns a list of user's locations. Though, if we retrieve User and Location objects from our database, and simply print the required information - the other information, such as the password will also be contained in that object. We won't print it, but it'll be there.

Let's make a Data Transfer Object to only transfer the required information. And while we're at it, let's aggregate the User and Location information, so that the data is transferred together:

public class UserLocationDTO {
    private Long userId;
    private String username;
    private double lat;
    private double lng;
    private String place;
    
    // Getters and Setters
} 

This object now contains all the information we want to show to the end-user. Now, we'll need a way to map the User and Location objects into a single UserLocationDTO object. This is typically done via mapping tools, such as MapStruct or ModelMapper, which we'll explore in latter sections.

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

For now, let's perform the conversion manually. Since we'll need a service that calls our UserRepository, we'll also map the results there and return the DTOs:

@Service
public class MapService {

    @Autowired
    private UserRepository userRepository;

    public List<UserLocationDTO> getAllUsersLocation() {
        return ((List<User>) userRepository
                .findAll())
                .stream()
                .map(this::convertToUserLocationDTO)
                        .collect(Collectors.toList());
    }

    private UserLocationDTO convertToUserLocationDTO(User user) {
        UserLocationDTO userLocationDTO = new UserLocationDTO();
        userLocationDTO.setUserId(user.getId());
        userLocationDTO.setUsername(user.getUsername());
        Location location = user.getLocation();
        userLocationDTO.setLat(location.getLat());
        userLocationDTO.setLng(location.getLng());
        userLocationDTO.setPlace(location.getPlace());
        return userLocationDTO;
}

Upon retrieving a list of Users, we directly convert them, alongside their Location information to UserLocationDTO objects. When calling this service, we'll retrieve this list of DTOs.

Finally, let's make a /map endpoint to allow someone to retrieve the location of the users:

@RestController
public class MapController {
  
    @Autowired
    private MapService mapService;

    @GetMapping("/map")
    @ResponseBody
    public List<UserLocationDTO> getAllUsersLocation() {
        List <UserLocationDTO> usersLocation = mapService.getAllUsersLocation();
        return usersLocation;
    }
}

This endpoint just returns a @ResponseBody. It can either be called by a user or by another service which parses the results.

Let's load up our database with some dummy information for testing purposes:

insert into location(id, lat, lng, place, description) values (1, 49.8, 24.03 ,'Lviv', 'Lviv is one of the largest and the most beautiful cities of Ukraine.');
insert into user(id, username, first_name, last_name, password, location_id) values (1, 'Romeo', 'Romeo', 'Montagues' ,'gjt6lf2nt5os', 1);
insert into user(id, username, first_name, last_name, password, location_id) values (2, 'Juliet', 'Juliet', 'Capulets' ,'s894mjg03hd0', 1);

Now, to test our endpoint, we'll use a tool like Postman to hit our endpoints:

Great! A list of our users is returned with only the required information both transferred and displayed.

We've written a mapping method within our MapService that aggregates and converts data, though, this process can easily be automated.

Mapping with ModelMapper

ModelMapper is a great mapping library that allows us to map between models and DTOs. It makes object mapping easy, by automatically determining how one object model maps to another.

To add it to a Maven project, we'd add the dependency:

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>${version}</version>
</dependency>

Or, if you're using Gradle:

compile group: 'org.modelmapper', name: 'modelmapper', version: '${version}'

Let's update our previous example with the ModelMapper library:

@Service
public class MapService {

    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private ModelMapper modelMapper;

    public List<UserLocationDTO> getAllUsersLocation() {
       return ((List<User>) userRepository
                .findAll())
                .stream()
                .map(this::convertToUserLocationDTO)
                .collect(Collectors.toList());
    }

    private UserLocationDTO convertToUserLocationDTO(User user) { 
        modelMapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.LOOSE);
        UserLocationDTO userLocationDTO = modelMapper
                .map(user, UserLocationDTO.class);	
        return userLocationDTO;
    }
}

Now, instead of the entire assignment process we've had to do before - we just map() a user to the UserLocationDTO. The method will flatten the properties of User within a UserLocationDTO and both the user information and the location will be present.

Note: When working with objects as properties, like our Location is a property of User, the standard matcher of the library may not be able to match all the properties. We've set the matching strategy to LOOSE to make it easier for the library to locate and match properties.

Mapping with MapStruct

MapStruct is an open-source Java-based code generator which creates code for mapping implementations.

It uses annotation-processing to generate mapper class implementations during compilation and greatly reduces the amount of boilerplate code which would regularly be written by hand.

If you're using Maven, install MapStruct by adding the dependency:

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>

This dependency will import the core MapStruct annotations. Since MapStruct works on compile-time and is attached to builders like Maven and Gradle, we'll also have to add a plugin to the <build>:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

If you're using Gradle, installing MapStruct is as simple as:

plugins {
    id 'net.ltgt.apt' version '0.20'
}

// Depending on your IDE
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'

dependencies {
    compile "org.mapstruct:mapstruct:${mapstructVersion}"
    annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}

We have our User and Location classes already, so let's make a mapper for those:

@Mapper
public interface UserLocationMapper {
    UserLocationMapper INSTANCE = Mappers.getMapper(UserLocationMapper.class);

    @Mapping(source = "user.id", target = "userId")
    UserLocationDTO toDto(User user, Location location);
}

When you build the project, MapStruct will pick up this @Mapper and generate a UserLocationMapperImpl class with a fully-functioning implementation.

MapStruct has a wide variety of functionalities and an advanced set of features. If you're interested in reading more about it, we highly suggest reading our in-depth Guide to MapStruct in Java.

Conclusion

In this article, we reviewed the Data Transfer Object Design Pattern with its pros and cons. This pattern is really dedicated just for remote calls because the conversion from and to DTOs can be expensive.

Additionally, we've built a demo Spring Boot application and explored two popular mappers that can be used to simplify the process of mapping between models and DTOs.

You can find all the project code on GitHub.

Was this article helpful?

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms