Spring Boot

Capstone – Article 11: Expense Categories & Advanced Queries (Expense Tracker)

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

1. Introduction

Your Expense Tracker is secure and functional. Now we make it useful and powerful.

In this article, we will:

  • Improve category handling
  • Add filtering by category & date
  • Add pagination & sorting
  • Prepare the ground for reports

These are real-world backend requirements.


2. Category Handling Strategy

For now, we’ll keep categories simple and flexible:

  • Stored as String (e.g., FOOD, TRAVEL, RENT)
  • Validated at API layer
  • Can be converted to enum later

This avoids premature complexity.


3. Improving ExpenseRequest Validation

Update ExpenseRequest:

@NotBlank(message = "Category is required")
private String category;

@NotNull(message = "Expense date is required")
private LocalDate expenseDate;

Optional:

  • Enforce uppercase categories
  • Validate against allowed list later

4. Adding Advanced Repository Queries

Update ExpenseRepository:

public interface ExpenseRepository extends JpaRepository<Expense, Long> {

    List<Expense> findByUserId(Long userId);

    List<Expense> findByUserIdAndCategory(Long userId, String category);

    List<Expense> findByUserIdAndExpenseDateBetween(
            Long userId, LocalDate start, LocalDate end);
}

Spring Data derives queries automatically.


5. Adding Pagination & Sorting

Use Pageable instead of List.

5.1 Repository Support

Page<Expense> findByUserId(Long userId, Pageable pageable);

5.2 Service Layer Pagination

public Page<ExpenseResponse> getMyExpenses(Pageable pageable) {

    String email = SecurityUtil.getCurrentUserEmail();
    User user = userRepository.findByEmail(email)
        .orElseThrow(() -> new UserNotFoundException("User not found"));

    return expenseRepository.findByUserId(user.getId(), pageable)
        .map(this::mapToResponse);
}

6. Filtering Expenses by Category

public List<ExpenseResponse> getExpensesByCategory(String category) {

    String email = SecurityUtil.getCurrentUserEmail();
    User user = userRepository.findByEmail(email)
        .orElseThrow(() -> new UserNotFoundException("User not found"));

    return expenseRepository
        .findByUserIdAndCategory(user.getId(), category)
        .stream()
        .map(this::mapToResponse)
        .toList();
}

7. Filtering Expenses by Date Range

public List<ExpenseResponse> getExpensesBetweenDates(
        LocalDate start, LocalDate end) {

    String email = SecurityUtil.getCurrentUserEmail();
    User user = userRepository.findByEmail(email)
        .orElseThrow(() -> new UserNotFoundException("User not found"));

    return expenseRepository
        .findByUserIdAndExpenseDateBetween(user.getId(), start, end)
        .stream()
        .map(this::mapToResponse)
        .toList();
}

8. Updating ExpenseController

@GetMapping("/filter/category")
public List<ExpenseResponse> getByCategory(@RequestParam String category) {
    return expenseService.getExpensesByCategory(category);
}

@GetMapping("/filter/date")
public List<ExpenseResponse> getByDateRange(
        @RequestParam LocalDate start,
        @RequestParam LocalDate end) {
    return expenseService.getExpensesBetweenDates(start, end);
}

@GetMapping
public Page<ExpenseResponse> getMyExpenses(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size) {

    Pageable pageable = PageRequest.of(page, size, Sort.by("expenseDate").descending());
    return expenseService.getMyExpenses(pageable);
}

9. Testing Advanced Queries

Examples:

GET /expenses?page=0&size=5
GET /expenses/filter/category?category=FOOD
GET /expenses/filter/date?start=2026-01-01&end=2026-01-31

Verify:

  • Only logged-in user data returned
  • Pagination works
  • Sorting works

10. Why This Matters in Interviews

This article demonstrates: ✔ Pagination knowledge
✔ Filtering & querying skills
✔ Practical API design
✔ Real business logic

Interviewers expect this level of thinking.


11. Common Mistakes

❌ Returning all data without pagination
❌ Filtering in controller
❌ Ignoring date handling
❌ Mixing business logic & security


12. Git Commit (Important)

git add .
git commit -m "Add expense filtering, pagination, and sorting"

13. What You Should Have Now

At this point:

  • Expense APIs are flexible
  • Data is paginated
  • Filtering works by category & date
  • Backend feels production-ready

14. What’s Next?

➡ Capstone – Article 12: Reporting & Aggregations

  • Monthly expense summary
  • Category-wise totals
  • Dashboard-ready APIs

Type Next when you’re ready 🚀