Spring Boot

Capstone – Article 9: Expense Entity & CRUD APIs (Expense Tracker)

By Utility Zone · 2026-01-27T18:32:49.030511

1. Introduction

This article builds the core business feature of the Expense Tracker.

So far we have:

  • Users
  • Authentication & authorization
  • Secure APIs

Now we add:

  • Expense entity
  • User → Expense relationship
  • CRUD APIs for expenses

This is where the application becomes truly useful.


2. Expense Domain Model

An expense typically contains:

  • Amount
  • Description
  • Category
  • Date
  • Owner (User)

Each expense belongs to exactly one user.


3. Creating Expense Entity

3.1 Create Entity Package (Already Exists)

com.example.expensetracker.entity

3.2 Create Expense Entity

package com.example.expensetracker.entity;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;

@Entity
@Table(name = "expenses")
public class Expense {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private BigDecimal amount;

    private String description;

    @Column(nullable = false)
    private String category;

    @Column(nullable = false)
    private LocalDate expenseDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    public Expense() {}

    // getters and setters
}

4. Understanding Relationships

@ManyToOne
private User user;

Meaning:

  • Many expenses → One user
  • Foreign key: user_id
  • Expenses are owned by users

This is critical for data security.


5. Creating ExpenseRepository

package com.example.expensetracker.repository;

import com.example.expensetracker.entity.Expense;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ExpenseRepository extends JpaRepository<Expense, Long> {

    List<Expense> findByUserId(Long userId);
}

Allows fetching expenses per user.


6. Creating Expense DTOs

6.1 ExpenseRequest

package com.example.expensetracker.dto;

import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;

public class ExpenseRequest {

    @NotNull
    private BigDecimal amount;

    private String description;

    @NotNull
    private String category;

    @NotNull
    private LocalDate expenseDate;

    // getters and setters
}

6.2 ExpenseResponse

package com.example.expensetracker.dto;

import java.math.BigDecimal;
import java.time.LocalDate;

public class ExpenseResponse {

    private Long id;
    private BigDecimal amount;
    private String description;
    private String category;
    private LocalDate expenseDate;

    // getters and setters
}

7. Creating ExpenseService

@Service
public class ExpenseService {

    private final ExpenseRepository expenseRepository;
    private final UserRepository userRepository;

    public ExpenseService(ExpenseRepository expenseRepository,
                          UserRepository userRepository) {
        this.expenseRepository = expenseRepository;
        this.userRepository = userRepository;
    }

    public ExpenseResponse createExpense(Long userId, ExpenseRequest request) {

        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("User not found"));

        Expense expense = new Expense();
        expense.setAmount(request.getAmount());
        expense.setDescription(request.getDescription());
        expense.setCategory(request.getCategory());
        expense.setExpenseDate(request.getExpenseDate());
        expense.setUser(user);

        Expense saved = expenseRepository.save(expense);
        return mapToResponse(saved);
    }

    public List<ExpenseResponse> getExpensesForUser(Long userId) {
        return expenseRepository.findByUserId(userId)
            .stream()
            .map(this::mapToResponse)
            .toList();
    }

    private ExpenseResponse mapToResponse(Expense expense) {
        ExpenseResponse response = new ExpenseResponse();
        response.setId(expense.getId());
        response.setAmount(expense.getAmount());
        response.setDescription(expense.getDescription());
        response.setCategory(expense.getCategory());
        response.setExpenseDate(expense.getExpenseDate());
        return response;
    }
}

8. Creating ExpenseController

@RestController
@RequestMapping("/expenses")
public class ExpenseController {

    private final ExpenseService expenseService;

    public ExpenseController(ExpenseService expenseService) {
        this.expenseService = expenseService;
    }

    @PostMapping
    public ExpenseResponse createExpense(@RequestBody @Valid ExpenseRequest request,
                                         @AuthenticationPrincipal(expression = "principal") String email) {
        // user lookup by email will be refined later
        Long userId = 1L;
        return expenseService.createExpense(userId, request);
    }

    @GetMapping
    public List<ExpenseResponse> getMyExpenses() {
        Long userId = 1L;
        return expenseService.getExpensesForUser(userId);
    }
}

Note:

  • Temporary user resolution
  • Will be improved in next article

9. Testing Expense APIs

Create Expense

POST /expenses
Authorization: Bearer <token>

Body:

{
  "amount": 1200.50,
  "description": "Groceries",
  "category": "Food",
  "expenseDate": "2026-01-25"
}

Get Expenses

GET /expenses
Authorization: Bearer <token>

10. Verify in H2 Console

SELECT * FROM EXPENSES;

You should see:

  • Expense records
  • Correct user_id

11. Important Design Notes

✔ Expense always tied to a user
✔ No user can access another user's expenses
✔ Service layer enforces ownership


12. Git Commit (Important)

git add .
git commit -m "Add Expense entity and CRUD APIs"

13. What You Should Have Now

At this point:

  • Expense entity exists
  • User–expense mapping works
  • CRUD APIs are functional
  • Core business feature implemented

14. What’s Next?

➡ Capstone – Article 10: Securing Expenses to Logged-In User

  • Extract user from JWT
  • Remove hardcoded userId
  • Real ownership enforcement

Type Next when you’re ready 🚀