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_idexpenseDatecategory
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 🚀