Mohammad YahiaMohammad Yahia
Back to all articles

Common Pitfalls for Java Enterprise Applications

A practical walkthrough of recurring architecture, microservice, caching, and ORM pitfalls in Spring-based enterprise systems.


Intro

I have seen a lot Java enterprise applications, especially in the Spring / Spring Boot ecosystem. Across different teams, companies, and system sizes. Long enough to notice that the same patterns repeat themselves again and again — even with smart people, good intentions, and modern tools. This document combines a short internal series into one narrative. It’s not about blaming tools. It’s about understanding how convenience, defaults, and pressure shape real systems.

1 – When Layered Architecture Turns into Procedural Chaos

Every Spring Boot project starts optimistically. The familiar structure:

Controller → Service → Repository

We feel organized. We feel “properly layered”. The service layer is where the business logic belongs… right? Then reality arrives:

  • more features
  • more edge cases
  • more deadlines
  • more “just add this quickly please” Suddenly, that innocent service is no longer small:
  • it grows to 10k+ lines
  • some methods reach 700–1000 lines
  • deeply nested if / else everywhere
  • giant switch statements
  • boolean flags controlling everything
  • method calling method calling method… Nobody designed it this way. It just… grew. At this point, it’s no longer really object-oriented. It becomes procedural code disguised as OOP. We still have:
  • “entities”
  • “services”
  • “repositories” But in reality:
  • entities are just DTOs with getters/setters
  • services are gigantic singleton scripts
  • business logic is scattered everywhere

The real pain: debugging

A request enters as a DTO.

  • It passes through 8–10 methods.
  • Every method mutates it a little.
  • Fields are added, removed, renamed. At the end, you ask:
  • How did this object even become like this?
  • Who changed this field? Nobody knows — without stepping through everything. Add a new edge case? Sure — another if. Except now that edge case exists in 12 different places. The results:
  • fragile tests
  • painful onboarding
  • “don’t touch this file” zones
  • no one understands the full picture What we really have:
  • god services
  • anemic domain models
  • uncontrolled mutation
  • logic scattered everywhere This usually doesn’t happen because developers are bad. It happens because:
  • the default stack nudges us this way
  • business pressure accelerates it Controller → Service → Repository is easy to start with… and very hard to keep healthy.

2 – The Entity Microservice Anti-Pattern

When people hear “microservices”, the first idea is often:

“Let’s make every table a service.”

UserService
ProductService
CompanyService

It feels modern. It feels scalable. It feels trendy. But what we often get is the cost of microservices without the benefits. What changes:

  • database joins → network calls
  • retries, timeouts, tracing everywhere
  • debugging becomes much harder Instead of:
  • simple systems
  • clear boundaries
  • autonomy We get:
  • distributed joins
  • chatty services
  • fragile flows

Performance bottlenecks appear quickly

A few entity services end up in the critical path of everything. ProductService and UserService get hammered — not because they’re slow, but because everything depends on them. So we:

  • scale them
  • add replicas
  • add caches But the pressure never really goes away. Because the boundary itself is wrong.

The deeper problem

These services expose:

  • entities, not behavior
  • internals, not business decisions Instead of asking:

“Is this user allowed to do X?” Callers must:

  • fetch data
  • assemble meaning
  • re-implement rules Once these APIs are exposed, people cling to them. They depend on your fields and structures. Later, when you say:

“Just ask for the behavior” The answer is: “No, we already rely on this.” So we end up with:

  • no real autonomy
  • no encapsulation
  • no clear ownership Just a distributed version of the same old problems.

3 – When Caching Exposes the Architecture

Spring Boot makes caching feel trivial:

  • add @Cacheable
  • configure Redis
  • run the app It feels like:

“We now have caching” In distributed systems, this is often a false sense of safety.

What actually happened

Each service calling another service added its own cache:

  • same Redis instance
  • same data
  • different keys
  • different TTLs So instead of one cache, we had many fragmented caches.

Then reality hit

When caches expired — especially with aligned TTLs:

  • multiple services missed at once
  • classic cache stampede / thundering herd
  • downstream service overwhelmed
  • database became the bottleneck Even inside a single service:
  • hundreds of concurrent cache misses
  • all rebuilding the same data
  • all hitting the database So yes — we “had caching”… but we didn’t get the benefit. The real issue wasn’t Redis or Spring Cache. The real issue was:
  • no clear authority for the data
  • no single owner of caching strategy Caching isn’t just optimization. It’s an architectural decision.

4 – ORM Performance Trap

Spring Boot also makes persistence feel magical. You don’t write SQL anymore. You just call:

getById()
findAll()
findSomethingBySomething()

It feels productive and clean. Until production.

A common trap

Developers stop thinking in queries and start thinking in methods. But every method still becomes SQL — whether you inspect it or not. This leads to:

  • fetching all rows, then filtering in Java
  • unnecessary joins
  • over-fetching columns
  • classic N+1 problems Everything works… until traffic grows and the database starts screaming.

Connection pooling & defaults

Spring Boot uses HikariCP, which is excellent — but defaults are not architecture. If you don’t understand or tune:

  • pool size
  • concurrency
  • connection lifetime You’ll see:
  • threads waiting for connections
  • slow requests
  • timeouts A major contributor here is Open-EntityManager-In-View, which can keep DB connections open for the entire HTTP request.

The recurring pattern

Spring Boot optimizes for:

  • fast startup
  • developer convenience Enterprise systems require:
  • explicit boundaries
  • predictable resource usage
  • conscious tuning ORMs don’t remove SQL. Annotations don’t remove cost. Defaults don’t remove responsibility.

Final Thoughts

This document isn’t anti-Spring. It isn’t anti-Hibernate. And it’s not theoretical. It’s about recognizing a pattern:

Convenience helps you start fast, but without understanding, it silently accumulates cost. Spring Boot makes it easy to begin. Enterprise-grade systems demand that we understand what’s really happening under the hood.