MyBatis vs JPA: There Is No Such Thing as a Bad Technology.

“What on earth is this project supposed to be?”

The project at my current company is what you might call a ‘frankenstack.’ MyBatis and JPA coexist in the same project. The problem was that all of the original creators who wrote this code had already left the company.

Those of us left behind, the current team, were confused. There was not a single document explaining why the technologies had been mixed that way. As a result, the situation only grew more chaotic.

In the end, inside one service, some people were digging through XML to build similar features while others were designing entities. It had turned into a bizarre state of ‘technical anarchy.’ The project was becoming a monster that was harder and harder to maintain, and eventually a meeting was called.

“We should standardize on a single tech stack.”

When technologies are mixed without principles, that is not freedom. It is just confusion.

“Isn’t JPA too slow to use?”

The room was tense. In particular, teammates who were used to MyBatis strongly opposed standardizing on JPA. Their main argument was simple: speed.

“Remember that statistics page we built with JPA that took 20 seconds just to load? If we write the SQL ourselves, it comes back in one second. It’s risky to rely on a black box we can’t fully control.”

It was true that some logic was taking dozens of seconds just to load data. But I was convinced that was not a problem with the technology. It was a problem with our level of proficiency. Right there in the meeting, I opened my laptop and pulled up the infamous ‘20-second code.’

The code was a mess. It queried a List<Entity>, and every time the loop touched a related object, it hit the database again, one by one. It was a textbook N+1 problem.

“This doesn’t mean JPA is slow. It means we made JPA work inefficiently. Watch this.”

On the spot, I applied a Fetch Join and reduced the query down to a single hit. After deployment, the 20-second load time dropped to 1 second. That was the moment I saw my teammates’ expressions change.

Before blaming the tool, read the manual first.

An Unexpected Discovery: Not a Mutant, but a Hybrid

Once the misunderstanding around JPA was cleared up, should we simply standardize everything on JPA? Once we looked closer, that was not the full answer either.

When we tried to handle complex reporting queries or Excel downloads with hundreds of thousands of rows through JPA, the cost of object mapping was too high, and building dynamic queries was awkward. MyBatis, on the other hand, was overwhelmingly more intuitive and faster for that kind of work. I started wondering, “How do other companies solve this?” Frustrated, I spent the night searching Google and digging through tech blogs.

To my surprise, many tech companies were not insisting on JPA alone. Some used QueryDSL for complex read performance, and some, just like us, used MyBatis alongside it.

That was when it clicked. “So the people who left didn’t mix these technologies thoughtlessly after all.”

They already understood the strengths and weaknesses of each tool and had deliberately separated responsibilities by technology. The real issue was that they left without documenting those decisions, so we, the people who inherited the system, ended up fighting over why it had never been unified.

Going Beyond JPA’s Limits: QueryDSL and Projection

Then another question came up. Could we remove MyBatis and solve the same problem using only technologies from the JPA ecosystem? That is where QueryDSL and Projection come in.

// 1. Interface that declares only the fields you need (no DTO required)
public interface DailyStat {
    String getDate();
    Long getTotalSales();
}

// 2. Fetch through QueryDSL or a JPA Repository
// Only those two columns are SELECTed from the DB, so its as fast as MyBatis.
List<DailyStat> stats = repository.findDailySales();

Once I understood those tools, there was far less reason to go back to XML hell with MyBatis. It turned out that high-performance queries were fully possible within the JPA ecosystem too.

Practical Advice: Stop Fighting and Let Them Coexist

In the end, our team chose ‘principled coexistence’ instead of ‘absolute standardization.’ (And in the long run, we aimed to move toward QueryDSL.)

The interesting part is that this survival strategy we chose, ‘JPA for writes, a specialized tool for reads,’ is, from an architectural perspective, a very basic form of the CQRS(Command and Query Responsibility Segregation) pattern.

You do not need to split your database in some grand way. Even just adopting the philosophy of separating command and query responsibilities at the code level makes a project dramatically cleaner.

In Closing: Technology Is Not Guilty

We often argue, “JPA is the best,” or, “No, MyBatis is what really works in production.” But what I learned from this experience is that there is no such thing as a bad technology. There are only situations where a technology is used for the wrong job.

JPA is like a magic wand, but if you cast the spell wrong, it blows up. MyBatis is like a sturdy hammer, but it can make everything look like a nail.

If your team is struggling right now because of legacy code or because people have different technology preferences, pause for a moment. There may be deeper reasoning from the developers who came before you hidden inside that confusion. Finding it and turning it into clear rules, that is how a team becomes genuinely strong.

Leave a Comment