DEV Community

marian-varga
marian-varga

Posted on • Originally published at dastalvi.com

What can happen if you skip the DTOs

It is nice that a framework like SpringBoot can do so many things for you.

You just need a JPA entity class plus a simple repository interface and SpringData gives you all you need for typical CRUD database operations.

You write a simple REST controller class and you have a REST API running, right?

Hey, but you forgot to write a DTO! But why do you actually need it when your app could work without it?

There are certainly some general reasons:

  • layered structure (e.g. hexagonal architecture or ports and adapters): for maintainability it is a good idea to decouple the external communication code form the core (business logic)
  • security and performance: if you expose the database structure in your API as-is, you will soon get to a point where you expose more than needed; that can be misused by malicious actors or waste resources (CPU, memory and network bandwidth)
  • DTOs, unlike JPA entities, can be immutable (you can use Java records) and that is good for data-driven (functional) programming style, nice unit tests, safer concurrency etc.

But other strange things can happen, too. I will show you one weird example based on my experience.

This GitHub repo contains a simple application that works without the DTOs. There is a User entity, each User can have multiple Transactions. We even have a Service bean between the repository and the RestController, catching possible database access exceptions.

As we want to make a production-ready application, we do not want Hibernate to generate the DDL. Instead, we have a schema.sql that creates the tables (later we may switch to Flyway or Liquibase). For our simple example, we also have a data.sql so that our tables are not empty.

When we run the application and call the API endpoint at http://localhost:8080/users, we get the expected JSON containing the users and their transactions.

Now let's pay attention to the two lines of code in the Transaction class, marked //!!

@JsonIgnore //!!

The first smell is that in the Transaction class we had to add the @JsonIgnore annotation to the User reference. Without that annotation the JSON serialization crashes due to infinite recursion.

Now let's imagine that someone makes a mistake by adding another field (description) to the Transaction entity, but forgets to adjust the SQL statements (or runs the application against an environment where the schema change has not been applied).

private String description;//!!

Of course, now the API call fails. But look at the error handling! The catch clause inside the UserService does not work as expected. Instead we can see a strange stack trace in the log:
GlobalExceptionHandler : Unexpected error org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON:

I once saw this situation (clearly, with an application much larger than this example) and it took me quite a while to understand why the SQL exception escaped the service and why I was getting an HttpMessageNotWritableException. Can you see it?

What happens is, the UserService class (via the UserRepository) only queries the USERS database table. The Transaction entities are not part of the result because of the default Hibernate lazy loading. Only when the Jackson deserializer tries to create JSON from the User instance, it invokes its getTransactions method that makes Hibernate fetch the Transaction entities.

This is why we get a strange stacktrace combining JSON and SQL stuff. The exception is caught by the GlobalExceptionHandler that does not know what to do with it, this is why the log message is "Unexpected error".

I hope this little exercise will make you understand more deeply how dangerous it is to allow different layers of your application to mix. Seeing just the "sunny day" scenarios of your application while it is still small may lead some developers to continue doing the wrong thing until it is too late.

You do not have to write the boilerplate code mapping the fields between your DTO and the other layers of your application. MapStruct can do it for you.

Top comments (0)