From CRUD to Business Logic
In the last module, we learned basic CRUD (Create, Read, Update, Delete). Now, we focus on business logic. This involves applying rules, performing calculations, and coordinating multiple steps. For example, a fund transfer isn't just one database write; it's a multi-step process that must succeed or fail as a single unit. This is where the Service layer truly shines!
5.1 Customer Management
The first step is to manage our customers. This involves creating a `Customer` entity and defining its relationship with the `Account` entity. A single customer can have multiple accounts. This is a classic One-to-Many relationship.
// --- Customer.java Entity ---
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// A Customer can have many Accounts.
// "mappedBy" tells JPA that the 'customer' field in the Account entity owns this relationship.
// CascadeType.ALL means if we delete a customer, all their accounts are deleted too.
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Account> accounts = new ArrayList<>();
// Getters and Setters...
}
// --- In Account.java Entity ---
// Add this relationship back to the Customer
@ManyToOne
@JoinColumn(name = "customer_id") // This creates a 'customer_id' foreign key column in the 'accounts' table.
private Customer customer;
API Endpoints for Customers
// --- CustomerController.java ---
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
@Autowired
private CustomerService customerService;
// Add a new customer
@PostMapping
public Customer createCustomer(@RequestBody Customer customer) {
return customerService.saveCustomer(customer);
}
// View customer details by ID
@GetMapping("/{id}")
public Customer getCustomerById(@PathVariable Long id) {
// The service would find the customer or throw an exception if not found.
return customerService.getCustomerById(id);
}
}
5.2 Account Management
Now we can build APIs for managing accounts, making sure each new account is properly linked to an existing customer.
API Endpoints for Accounts
// --- AccountController.java ---
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
@Autowired
private AccountService accountService;
// Open a new account FOR a specific customer
@PostMapping("/customer/{customerId}")
public Account openAccount(@PathVariable Long customerId, @RequestBody Account account) {
return accountService.createAccountForCustomer(customerId, account);
}
// View account balance
@GetMapping("/{accountId}/balance")
public double getAccountBalance(@PathVariable Long accountId) {
return accountService.getAccountBalance(accountId);
}
}
5.3 Transactions: The Core Business Logic
This is the heart of our banking application. We'll implement the logic for depositing, withdrawing, and transferring money. These operations must be transactional—they must complete fully or not at all. We use the @Transactional annotation on our service methods to ensure this data integrity.
Deposit and Withdraw Logic
These methods will now be housed in our `AccountService`.
// --- In AccountService.java ---
@Transactional
public Account deposit(Long accountId, double amount) {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new RuntimeException("Account not found"));
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive.");
}
account.setBalance(account.getBalance() + amount);
return accountRepository.save(account);
}
@Transactional
public Account withdraw(Long accountId, double amount) {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new RuntimeException("Account not found"));
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive.");
}
if (account.getBalance() < amount) {
throw new RuntimeException("Insufficient funds.");
}
account.setBalance(account.getBalance() - amount);
return accountRepository.save(account);
}
Fund Transfer Logic
This is our most complex piece of business logic. It involves two accounts and must be handled as a single atomic operation.
// --- In AccountService.java ---
@Transactional
public void transfer(Long fromAccountId, Long toAccountId, double amount) {
// 1. Debit from the source account (reusing our withdraw logic)
withdraw(fromAccountId, amount);
// 2. Credit to the destination account (reusing our deposit logic)
deposit(toAccountId, amount);
System.out.println("Transfer successful!");
}
@Transactional is crucial here: Imagine if the `withdraw` call succeeds, but the application crashes before the `deposit` call is made. The money would vanish! The @Transactional annotation ensures that if any part of the `transfer` method fails, the initial withdrawal is "rolled back," and the database is left in its original state.
5.4 Transaction History API
A bank must keep a record of every transaction. To do this, we'll create a new `Transaction` entity and modify our service methods to log every financial operation.
The Transaction Entity
@Entity
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String type; // e.g., "DEPOSIT", "WITHDRAWAL", "TRANSFER"
private double amount;
private LocalDateTime timestamp;
@ManyToOne
@JoinColumn(name = "account_id")
private Account account;
// Constructors, Getters, and Setters...
}
Updating the Service to Log Transactions
We would now modify our `deposit` and `withdraw` methods to create and save a `Transaction` object each time they are successfully executed.
// --- Example modification in AccountService.deposit() ---
// ... after successfully updating the balance ...
Transaction transaction = new Transaction("DEPOSIT", amount, LocalDateTime.now(), account);
transactionRepository.save(transaction); // Assume we have a TransactionRepository
return savedAccount;
API Endpoint for History
Finally, we create a repository method and controller endpoint to fetch the history for a specific account.
// --- TransactionRepository.java ---
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
// Spring Data JPA automatically creates the query from the method name!
List<Transaction> findByAccountIdOrderByTimestampDesc(Long accountId);
}
// --- In AccountController.java ---
@GetMapping("/{accountId}/transactions")
public List<Transaction> getTransactionHistory(@PathVariable Long accountId) {
return transactionService.getTransactionsForAccount(accountId);
}