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 🚀