“Data duplication is absolute evil”
In my university database class, the professor emphasized it so hard that spit was flying. “First normal form, second normal form, third normal form… duplicated data wastes storage space and damages integrity. Split it up, and then split it again!”
I followed that teaching faithfully. During my graduation project, I split tables into 10, then 20 separate pieces. ‘User’, ‘Address’, ‘City’, ‘Zipcode’… My database design was so perfectly “by the book” that storing a single address required three different tables.
But in real-world work, that perfect design turned into a disaster. I only wanted to fetch a single customer list, and yet I had to write five JOINs. The query became complicated, performance got slower, and worst of all, moving that data into Java objects was painfully cumbersome.
“Why is the code this complicated when I’m just trying to fetch one row of data to show on the screen?”
Only then did I realize it. In school, they taught us how to optimize for storing data. In real work, optimizing reads matters far more. And between an object-oriented language like Java and a relational database, there flows a river that is much harder to cross than I had imagined.

Paradigm mismatch: squares and circles
The fundamental cause of this pain is what we call an impedance mismatch between paradigms.
- Java (objects): A graph structure. ‘Member’ has a ‘Team’, and inside ‘Team’ there is a ‘List<Member>’ again. You move around freely by following references.
- DB (tables): A grid, like an Excel sheet. Tables are connected through foreign keys (FK), but at heart they are strangers. To combine them, you have to pay the steep price of a ‘JOIN’.
Back in college, I forced these two worlds together by writing SQL directly. I broke Java objects apart to store them in the database with INSERTs, then pulled rows back out with SELECT, reading them line by line from a ResultSet and stuffing them one by one into Java collections like Set or List. I did not feel like a developer. I felt like a data translator.
What appeared to solve that tedious repetition was JPA (Java Persistence API), in other words, ORM (Object-Relational Mapping).
JPA and ORM: handling databases through objects
ORM literally means a technology that maps objects and relational databases together. As the term suggests, the key idea is that you define and work with database tables as if they were Java objects.
We no longer need to handwrite CREATE TABLE queries. Instead, we create a Java class and attach a sticker called @Entity. Then JPA, the Java standard for ORM, looks at that class and says, “Ah, so this is the shape of the table you need,” and creates the table in the database automatically.
Saving data also stops being a matter of writing SQL. You can just write something like repository.save(member), the way you would add something to a Java collection. The developer stays completely in an object-oriented way of thinking, while the messy SQL translation work gets pushed onto JPA.
But as we learned in the Re: Booting series, convenience always has a price. Because I trusted that automatic translator called JPA too much, I planted a time bomb in my code: the N+1 problem.
[Code Verification] The N+1 problem, a bomb made of queries
This is a problem that practically every junior developer runs into the first time they use JPA. The situation is simple: “Print every member together with the name of the team that member belongs to.”
// 1. Fetch all members (1 query fired)
List<Member> members = memberRepository.findAll();
for (Member member : members) {
// 2. Print each members team name
// If there are 100 members, 100 extra queries run to fetch team info!
System.out.println(member.getTeam().getName());
}
The query we expected: SELECT * FROM Member JOIN Team ... (just once)
The query that actually happened:
SELECT * FROM Member(fetches the member list, 1 query)SELECT * FROM Team WHERE id = 1(looks up the first member’s team)SELECT * FROM Team WHERE id = 2(looks up the second member’s team)- … (repeats once for every member)
If there are 100 members, the database gets hit with 101 queries: 1 + N. If there are 10,000 members? Then 10,001 queries slam into the database. That is the infamous N+1 problem, the kind of issue that can bring down a server. JPA tried to be convenient by loading related data lazily, one piece at a time, and that very convenience turned into the disaster.

Practical advice: pragmatic database design
So what should we do in real work? We need to strike a balance between textbook normalization and the convenience of JPA.
- Make Fetch Join your go-to: The surest way to solve the N+1 problem is to tell JPA explicitly, “Fetch everything together from the start.” If you use ‘join fetch’ in JPQL, the data comes back in a single query, just like a ‘JOIN’ in SQL.
- Sometimes ‘de-normalization’ is the answer: Your professor would not like it, but in real-world work, duplicate data is sometimes allowed for the sake of performance. For example, if a ‘post list’ needs to show the ‘author name’, instead of joining all the way to the ‘User’ table, you just store a duplicate ‘writer_name’ column in the ‘Post’ table. It is a strategy of spending storage space (Disk) to buy read speed (Speed).
- Eager loading (EAGER) is poison: The moment you write ‘@ManyToOne(fetch = FetchType.EAGER)’ in your JPA settings, the gates of hell open, because it drags in everything, even data you don’t need. In real work, always make ‘lazy loading (LAZY)’ the default and use ‘Fetch Join’ only when you need it.
Closing: to get more convenience, you have to know more
JPA is absolutely a revolution. It freed us from the mind-numbing repetition of writing SQL over and over again. But thinking, “Since I use JPA now, I don’t need to know SQL anymore,” is dangerous.
JPA is not a magician. It is just a secretary that writes SQL on your behalf. If you give bad instructions to that secretary, through a poor mapping, eager loading, and so on, the secretary will quietly fire off one hundred queries and bring down the database. To monitor and tune whether the SQL generated by JPA is actually efficient, you paradoxically need to understand SQL more deeply. Comfort always comes with responsibility.
Now we know how to put data into objects. But can we just send those objects, those entities, straight to the frontend, to Vue.js? What if the User entity contains a password? And what happens to security if we send the frontend information it was never supposed to receive?
Next time, let’s talk about DTOs, Data Transfer Objects, and REST API design, the techniques used to package data safely for delivery.


