money/README.md
Encapsulate monetary values and their associated currency in a domain-specific object.
Real-world example
Imagine an online gift card system, where each gift card holds a specific balance in a particular currency. Instead of just using a floating-point value for the balance, the system uses a Money object to precisely track the amount and currency. Whenever someone uses the gift card, it updates the balance with accurate calculations that avoid floating-point rounding errors, ensuring the domain logic stays consistent and accurate.
In plain words
The Money pattern encapsulates both an amount and its currency, ensuring financial operations are precise, consistent, and maintainable.
Wikipedia says
The Money design pattern encapsulates a monetary value and its currency, allowing for safe arithmetic operations and conversions while preserving accuracy and consistency in financial calculations.
Mind map
Flowchart
In this example, we're creating a Money class to demonstrate how monetary values can be encapsulated along with their currency. This approach helps avoid floating-point inaccuracies, ensures arithmetic operations are handled consistently, and provides a clear domain-centric way of working with money.
@AllArgsConstructor
@Getter
public class Money {
private double amount;
private String currency;
public Money(double amnt, String curr) {
this.amount = amnt;
this.currency = curr;
}
private double roundToTwoDecimals(double value) {
return Math.round(value * 100.0) / 100.0;
}
public void addMoney(Money moneyToBeAdded) throws CannotAddTwoCurrienciesException {
if (!moneyToBeAdded.getCurrency().equals(this.currency)) {
throw new CannotAddTwoCurrienciesException("You are trying to add two different currencies");
}
this.amount = roundToTwoDecimals(this.amount + moneyToBeAdded.getAmount());
}
public void subtractMoney(Money moneyToBeSubtracted) throws CannotSubtractException {
if (!moneyToBeSubtracted.getCurrency().equals(this.currency)) {
throw new CannotSubtractException("You are trying to subtract two different currencies");
} else if (moneyToBeSubtracted.getAmount() > this.amount) {
throw new CannotSubtractException("The amount you are trying to subtract is larger than the amount you have");
}
this.amount = roundToTwoDecimals(this.amount - moneyToBeSubtracted.getAmount());
}
public void multiply(int factor) {
if (factor < 0) {
throw new IllegalArgumentException("Factor must be non-negative");
}
this.amount = roundToTwoDecimals(this.amount * factor);
}
public void exchangeCurrency(String currencyToChangeTo, double exchangeRate) {
if (exchangeRate < 0) {
throw new IllegalArgumentException("Exchange rate must be non-negative");
}
this.amount = roundToTwoDecimals(this.amount * exchangeRate);
this.currency = currencyToChangeTo;
}
}
By encapsulating all money-related logic in a single class, we reduce the risk of mixing different currencies, improve clarity of the codebase, and facilitate future modifications such as adding new currencies or refining rounding rules. This pattern ultimately strengthens the domain model by treating money as a distinct concept rather than just another numeric value.
Benefits
Trade-offs