Setting DTOs in Your Backend Service Can Reduce API Response Time From 6 Seconds to 40ms
Most of us think we add DTOs because we don’t want to send the entire entity back to the client.
The user only needs a name, an id, maybe a status. Why send everything, right?
That’s exactly how I thought too.
Turns out, that’s not even the main reason DTOs exist.
The real reason only shows up once your service gets a bit real. A bit slow. A bit scary.
Let’s talk about it.
Say you have a backend service. Plain old REST. Nothing fancy. You load a User from the database.
User user = userRepository.findById(id);Your User entity looks innocent enough.
@Entity
public class User {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders;
// getters
}Notice that LAZY.
Most people add it because “that’s the default” or “someone told me eager is bad”. But what does it actually do?
When Hibernate loads this User, it does not load the orders.
Not even a little.
There is no SELECT * FROM orders ... happening at this point.
Instead, Hibernate puts a placeholder there. A proxy. Something that looks like a list, behaves like a list, but isn’t actually holding any data yet.
So at this moment:
user.getOrders();has not been called. No query has happened. So far, so good.
Now comes the part nobody expects.
You return this entity directly from your controller.
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
}Spring sees this User and hands it to Jackson.
Jackson’s job is simple: convert this object into JSON. But Jackson doesn’t look at fields. Jackson looks at getters.
So internally, Jackson does something very close to this:
user.getId();
user.getName();
user.getOrders();And that last line is where things quietly go sideways.
Because now, for the very first time, getOrders() is called. Hibernate sees that call and goes,
“Oh. You actually want the orders now? Cool.”
And fires a SQL query.
SELECT * FROM orders WHERE user_id = ?This query happens during serialization.
Not in your service logic.
Not in your repository.
While JSON is being written.
Get Hesanda Nimneth Liyanage’s stories in your inbox
Join Medium for free to get updates from this writer.
And yes, the data is actually fetched, mapped into entities, and handed back to Jackson so it can keep serializing.
If this user has 5 orders, that’s one extra query.
If you return a list of 100 users?
Now it’s:
1 query for users
100 queries for orders
That’s the N+1 problem.
Why “+1”?
Because you start with 1 main query, and then for each row returned, you accidentally trigger N more queries.
Nothing crashes.No exception. No red flags.
Your endpoint just went from ~40ms to ~6 seconds and you’re staring at the logs wondering what changed.
And here’s the part that hurts a little.
Lazy loading itself isn’t broken.Hibernate is doing exactly what you asked.Jackson is also doing exactly what it’s designed to do.They just don’t know about each other.Jackson doesn’t care that orders is lazy. It sees a getter. It calls it.
Now enter DTOs.
A DTO is boring. And that’s the point.
public record UserDto(Long id, String name) {}No relationships.
No proxies.
No hidden database access.
You map it explicitly.
User user = userRepository.findById(id).orElseThrow();
UserDto dto = new UserDto(
user.getId(),
user.getName()
);Notice what’s missing.
No getOrders() !!!
Which means Jackson never gets the chance to accidentally wake Hibernate up and make it talk to the database.
Serialization becomes a pure in-memory operation.
No SQL.
No surprises.
That’s why DTOs suddenly turn a slow endpoint into a fast one without changing any queries in your repository. They’re not just about “sending less data”. They’re about controlling what code is allowed to execute during serialization.
Once you see that, a lot of weird production issues start to make sense.
That random slowdown after adding one field.
That endpoint that’s fast in tests but slow in production.
That “but I only made it lazy” moment.
DTOs aren’t ceremony.
They’re a safety boundary.
They stop your database from being touched at the worst possible time: while you’re already in the middle of responding to a request.
And once you realize serialization can trigger SQL, you never look at “just returning the entity” the same way again.










