DEV Community

araf
araf

Posted on

3 1 1

Spring Boot Anti-Patterns Killing Your App Performance in 2025 (With Real Fixes & Explanations)

Spring Boot helps developers move fast — but bad patterns kill performance, scalability, and maintainability silently.

Here’s a 2025-ready breakdown of real anti-patterns, why they’re dangerous, and how to fix them with clean, performant code.


⚠️ 1. Overusing @Transactional on Everything

❌ Anti-Pattern:

@Transactional
public List<User> getAllUsers() {
    return userRepository.findAll();
}
Enter fullscreen mode Exit fullscreen mode

🔍 Problem:

Even read-only queries run inside unnecessary transactions, which:

  • Lock resources
  • Block threads
  • Add overhead to the database

✅ Fix:

@Transactional(readOnly = true)
public List<User> getAllUsers() {
    return userRepository.findAll();
}
Enter fullscreen mode Exit fullscreen mode

🛠️ Explanation:

The readOnly = true hint allows the database to optimize the query plan, avoids write locks, and improves throughput for concurrent reads.


⚠️ 2. Business Logic in Controllers

❌ Anti-Pattern:

@PostMapping("/users")
public ResponseEntity<?> create(@RequestBody UserDTO dto) {
    if (dto.getAge() < 18) return ResponseEntity.badRequest().build();
    userRepository.save(new User(dto.getName(), dto.getAge()));
    return ResponseEntity.ok().build();
}
Enter fullscreen mode Exit fullscreen mode

🔍 Problem:

Mixing logic with HTTP code:

  • Hard to test
  • Difficult to scale
  • Breaks SRP (Single Responsibility Principle)

✅ Fix:

@Service
public class UserService {
    public User createUser(UserDTO dto) {
        if (dto.getAge() < 18) throw new IllegalArgumentException("Underage");
        return userRepository.save(new User(dto.getName(), dto.getAge()));
    }
}
Enter fullscreen mode Exit fullscreen mode

🛠️ Explanation:

Separating logic into a service layer:

  • Promotes clean architecture
  • Enables reuse across APIs or scheduled jobs
  • Makes unit testing easier

⚠️ 3. Blocking Calls in Reactive Code

❌ Anti-Pattern:

@GetMapping("/pdf")
public Mono<String> generatePdf() {
    return Mono.just(createHeavyPdf()); // Blocking!
}
Enter fullscreen mode Exit fullscreen mode

🔍 Problem:

Calling blocking I/O (e.g., file, DB, network) on Netty threads breaks the reactive model.

✅ Fix:

@GetMapping("/pdf")
public Mono<String> generatePdf() {
    return Mono.fromCallable(this::createHeavyPdf)
               .subscribeOn(Schedulers.boundedElastic());
}
Enter fullscreen mode Exit fullscreen mode

🛠️ Explanation:

Schedulers.boundedElastic() offloads blocking tasks to a separate thread pool — keeping the event loop free for I/O-bound processing.


⚠️ 4. Not Using Virtual Threads

❌ Anti-Pattern:

@Bean
public Executor asyncExecutor() {
    return Executors.newFixedThreadPool(10); // Limited concurrency
}
Enter fullscreen mode Exit fullscreen mode

✅ Fix (Java 22+):

@Bean
public Executor asyncExecutor() {
    return Executors.newVirtualThreadPerTaskExecutor();
}
Enter fullscreen mode Exit fullscreen mode

🛠️ Explanation:

Virtual threads are lightweight, memory-efficient, and allow thousands of concurrent tasks without blocking kernel threads — perfect for high-throughput apps.


⚠️ 5. Overusing EntityManager Manually

❌ Anti-Pattern:

@PersistenceContext
private EntityManager em;

public List<User> getUsers() {
    return em.createQuery("FROM User", User.class).getResultList();
}
Enter fullscreen mode Exit fullscreen mode

🔍 Problem:

  • Manual queries are error-prone.
  • You miss Spring Data’s features (pagination, filters, query derivation).

✅ Fix:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByStatus(String status);
}
Enter fullscreen mode Exit fullscreen mode

🛠️ Explanation:

Spring Data JPA is optimized, tested, and handles:

  • Transactions
  • Paging
  • Dynamic queries Better abstraction = fewer bugs, more performance.

⚠️ 6. @Cacheable Without Expiry

❌ Anti-Pattern:

@Cacheable("products")
public Product getProduct(Long id) {
    return productRepo.findById(id).orElseThrow();
}
Enter fullscreen mode Exit fullscreen mode

🔍 Problem:

Caches can grow unbounded, consuming memory.

✅ Fix:

@Bean
public CacheManager cacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager("products");
    cacheManager.setCaffeine(Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(1000));
    return cacheManager;
}
Enter fullscreen mode Exit fullscreen mode

🛠️ Explanation:

Adds TTL (Time to Live) and max entries — preventing memory leaks and stale data. Caffeine is ultra-fast and supports async refresh.


⚠️ 7. Default Tomcat Thread Pool

❌ Anti-Pattern:

# Default max-threads = 200
Enter fullscreen mode Exit fullscreen mode

🔍 Problem:

Your API could underperform under high load — threads wait in queue, requests time out.

✅ Fix:

server.tomcat.max-threads=500
server.tomcat.accept-count=100
server.connection-timeout=10s
Enter fullscreen mode Exit fullscreen mode

🛠️ Explanation:

  • max-threads: how many concurrent requests can be served
  • accept-count: queued connections
  • connection-timeout: prevents hanging sockets

Tuning Tomcat = faster concurrency handling


⚠️ 8. Overlapping Scheduled Jobs

❌ Anti-Pattern:

@Scheduled(fixedRate = 5000)
public void sendEmails() {
    // Takes 10s to run = jobs overlap = CPU spike
}
Enter fullscreen mode Exit fullscreen mode

✅ Fix with ShedLock:

@Scheduled(fixedRate = 5000)
@SchedulerLock(name = "emailJob", lockAtLeastFor = "PT5S")
public void sendEmails() {
    // Run once, skip overlap
}
Enter fullscreen mode Exit fullscreen mode

🛠️ Explanation:

ShedLock uses distributed locks (e.g., Redis, DB) to prevent concurrent job runs — crucial for long-running tasks.


⚠️ 9. No Observability

❌ Anti-Pattern:

No logs, no metrics, no traces:

// How do we know if it’s slow? 🤷
Enter fullscreen mode Exit fullscreen mode

✅ Fix:

Micrometer + Prometheus + Grafana

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
management.endpoints.web.exposure.include=health,info,metrics,prometheus
Enter fullscreen mode Exit fullscreen mode

🛠️ Explanation:

Gives real-time visibility into:

  • API latency
  • JVM memory
  • Cache hit/miss
  • DB time

Bonus: Add OpenTelemetry for distributed tracing across microservices.


🧠 Final Thoughts

Even in 2025, these old habits still slow down modern Spring Boot apps.

✅ Virtual threads
✅ Reactive practices
✅ Caching strategies
✅ Observability
✅ Clean architecture

Start reviewing your codebase for these traps — and refactor for performance, scalability, and developer sanity.


💬 What Anti-Pattern Do You See Most?

Comment below with real issues you've solved (or still suffer with). Let’s build a community playbook to clean up enterprise Spring Boot!

Top comments (0)