Spring Boot

Capstone – Article 13: Performance, Caching & Optimization (Expense Tracker)

By Utility Zone · 2026-01-27T18:33:33.365921

1. Introduction

Your Expense Tracker is now feature-rich and secure. The next step is making it fast, scalable, and production-ready.

In this article, we will:

  • Introduce caching
  • Optimize expensive operations
  • Improve overall application performance

These are senior-level backend concerns.


2. Why Performance Optimization Matters

Without optimization:

  • Reports become slow as data grows
  • Database load increases
  • User experience degrades

Performance tuning helps: ✔ Faster responses
✔ Reduced DB load
✔ Better scalability


3. Introducing Caching

Caching stores frequently used data in memory.

Typical candidates for caching:

  • Monthly reports
  • Category summaries
  • Read-heavy APIs

We will use Spring Cache Abstraction.


4. Enabling Caching

Add dependency (if not already present):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

Enable caching:

@SpringBootApplication
@EnableCaching
public class ExpenseTrackerApplication {
}

5. Caching Report APIs

5.1 Cache Monthly Summary

@Cacheable(
    value = "monthly-summary",
    key = "#month + '-' + #year + '-' + T(com.example.expensetracker.security.SecurityUtil).getCurrentUserEmail()"
)
public MonthlySummary getMonthlySummary(int month, int year) {
    User user = getLoggedInUser();
    return expenseRepository.getMonthlyTotal(user.getId(), month, year);
}

5.2 Cache Category Summary

@Cacheable(
    value = "category-summary",
    key = "#month + '-' + #year + '-' + T(com.example.expensetracker.security.SecurityUtil).getCurrentUserEmail()"
)
public List<CategorySummary> getCategorySummary(int month, int year) {
    User user = getLoggedInUser();
    return expenseRepository.getCategorySummary(user.getId(), month, year);
}

✔ Cache key includes user identity
✔ Prevents cross-user data leaks


6. Cache Eviction Strategy

Whenever expenses change, cached reports must be cleared.

@CacheEvict(
    value = {"monthly-summary", "category-summary"},
    allEntries = true
)
public ExpenseResponse createExpense(ExpenseRequest request) {
    // save expense
}

This ensures:

  • Cached data stays fresh
  • No stale reports

7. Choosing Cache Provider

Default:

  • ConcurrentMapCache (in-memory)

Production options:

  • Caffeine (high performance)
  • Redis (distributed cache)

For this project: ✔ Default cache is sufficient


8. Database Optimization Tips

✔ Add indexes on:

  • user_id
  • expenseDate
  • category

Example:

@Table(
    name = "expenses",
    indexes = {
        @Index(name = "idx_user_id", columnList = "user_id"),
        @Index(name = "idx_expense_date", columnList = "expenseDate")
    }
)

Indexes dramatically improve query speed.


9. Avoiding Common Performance Pitfalls

❌ Fetching too much data
❌ Missing pagination
❌ Aggregating in Java
❌ Over-caching everything

Always measure before optimizing.


10. Observing Performance Improvements

Test:

  • First report request → slow
  • Second request → fast (cache hit)

Logs will show:

  • DB query executed once
  • Cache serving subsequent calls

11. Git Commit (Important)

git add .
git commit -m "Add caching and performance optimizations for reports"

12. What You Should Have Now

At this point:

  • Reports are cached
  • Performance is optimized
  • Backend is scalable
  • App feels production-grade

13. What’s Next?

➡ Capstone – Article 14: Logging, Monitoring & Actuator

  • Structured logging
  • Health checks
  • Production observability

Type Next when you’re ready 🚀