Treat Data as Data

Object-oriented approach is a mighty concept making software more maintainable, which means cheaper and easier to understand. Problems come at boundaries, where objects have to be passed on into a different layer or another system. There, the objects become just data and should be treated as that.


This problem is not new, was already noticed by Mark Seemann and many others. Michael Nygard writes in Release It!:

"What appears as a class in one layer should be mere data to every other layer."

Objects in the domain layer are "living" active entities defined by their behavior. But what happens when an object needs to be persisted, or for example displayed on the screen? What is an object for a database? How is an object represented in a message or in a response of a REST call? It's mere data.

As an example, consider a REST controller returning an object as JSON. With Spring framework we typically do it as follows:

@GetMapping
public Account login(String username, String password) {
    return accounts.login(username, password);
}
Because Account provides a getUsername() method, the response looks like:

{
    "username": "test" 
}

The problem with this approach is that the domain layer (Account) is highly coupled to the application layer (controller): a change in the domain object can lead to a broken contract. Another problem is leaking of implementation details: adding a new getter to the domain object will change the response as follows:

{
    "username": "test",
    "password": "pwd1" 
}

Another obvious problem is, that Spring requires getters to convert the domain object to its serialized representation (e.g. JSON). This breaks encapsulation of the object and leads to a shift from the OOP understanding of objects as a unit of behavior to treating objects as poor data structures with a bunch of attached procedures.

Data is Data

The solution is to understand this gap and to treat objects as objects and data as data. A traditional way of doing this is known as Data Transfer Object (DTO):

@GetMapping
public LoginDTO login(String username, String password) {
    Account account = accounts.login(username, password);
    return new LoginDTO(account.getUsername());
}

class LoginDTO {

    private final String username;
    
    public LoginDTO(String username) {
        this.username = username;
    }
    
    public String getUsername() {
        return this.username;
    }
}

Creating a DTO for all use-cases is a lot of hard work. Is it really worth? What are the actual benefits of DTOs? I can think of two:

  1. Static typing
  2. Explicit structure

Static Typing

Static typing is a big benefit of strongly typed languages that brings a good level of confidence as a lot of bugs are discovered already during compilation. The problem here is, that no input/output is actually strongly typed. Consider the following endpoint:

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void register(@RequestBody ToRegisterDTO toRegister) {
    accounts.register(
        toRegister.getUsername(), 
        toRegister.getPassword(), 
        toRegister.getEmail());
}

static class ToRegisterDTO {

    private final String username;
    private final String password;
    private final String email;
    
    // constructor and getters...
}

The method expects the input in the following format:

{
    "username": ...,
    "password": ...,
    "email": ...
}

But there is nothing to prevent the client to send anything different:

{
    "UserName": ...,
    "pass": ...,
    "e-mail": ...
}

The typical solution is to validate the input, but no typing will help us here. So why should we bother?

Explicit Structure

Explicit is always good, but DTOs create a lot boiler-plate code and are not much different to pure data structures. Compare the following variants:

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void register(@RequestBody ToRegisterDTO toRegister) {
    accounts.register(
        toRegister.getUsername(), 
        toRegister.getPassword(), 
        toRegister.getEmail());
}

// vs.

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void register(@RequestBody Map<String, String> toRegister) {
    accounts.register(
        toRegister.get("username"), 
        toRegister.get("password"), 
        toRegister.get("email"));
}

In my opinion there is no big difference, in the second variant a lot of code for DTOs disappeared (the less to maintain the better) and the code tells the reader much more explicitly that data, no objects, are to be found here.

A Data Transfer Objects are, despite the name, no object at all. There are nothing more than strongly typed data structures. As the strong typing doesn't bring a great value, explicit data structures like Map and List could be used instead.

Conclusion

Whether using DTOs or standard data structures, dealing with data should be explicit and obvious from the code. Leaking domain beyond layer boundaries increase coupling and can undesirably effect the API. This risk should be avoided by treating data as data clearly and explicitly. Objects are units of behavior which can't cross layers. This fact should be reflected in the code.

The source code could be found on GitHub.

Happy coding!