⚠️ proof-of-concept: does not present a stable nor complete API yet
Type-safe money representation with reified currency types - combining familiar JDK primitives (BigDecimal, Currency) with lossless fraction arithmetic (BigRational) to keep calculations exact, and optionally capture rounding loss whenever you finally project results back into BigDecimal.
Money<USD> usd = Money.from(BigRational.of(10, 3), TypedCurrency.USD);
Rate<USD, PLN> rate = Rate.from("4.00", TypedCurrency.USD, TypedCurrency.PLN);
Money<PLN> pln = usd.convert(rate);
Decimal r = pln.amount().toDecimal(2, BigRational.Rounding.HALF_UP);
BigDecimal value = r.value(); // 13.33
BigRational loss = r.residual(); // 1/300 (~0.0033333333)
Instead of representing money as (BigDecimal, Currency) and hoping to not accidentally add EUR to USD or apply the wrong rate, typed-money models currency correctness explicitly:
TypedCurrency- a type-safe representation of a currency (reified as a Java type)Money<EUR>- money in a specific currency typeBigRational- exact rational arithmetic with deterministic rounding andBigDecimalconversion helpersConversionRate<EUR, USD>- a rate that can only exchangeMoney<EUR>intoMoney<USD>
Goal: push currency correctness into the type system (as far as Java generics allow) while staying ergonomic.
DirectionalCurrencyPair<USD, EUR> usdeur = DirectionalCurrencyPair.of(TypedCurrency.USD, TypedCurrency.EUR);
Money<USD> usdAmount = Money.from(BigDecimal.TEN, TypedCurrency.USD);
Money<EUR> eurAmount = Money.from(BigDecimal.TEN, TypedCurrency.EUR);
ConversionRate<USD, EUR> rate1 = Rate.from(new BigDecimal("0.84"), TypedCurrency.USD, TypedCurrency.EUR);
ConversionRate<USD, EUR> rate2 = Rate.from(new BigDecimal("0.84"), usdeur);
ConversionRate<EUR, USD> inverted = rate1.invert();
TypedCurrency currency = TypedCurrency.from("CHF");
switch (currency) {
case CHF chf -> System.out.println(chf);
default -> System.out.println("not chf");
}
Sometimes we don’t know the currency at compile time (e.g., parsed from a message). We can still use Money<TypedCurrency>, but then correctness is enforced at runtime:
Money<TypedCurrency> chf = Money.from(BigDecimal.TEN, TypedCurrency.from("CHF"));
Money<TypedCurrency> gbp = Money.from(BigDecimal.TEN, TypedCurrency.from("GBP"));
Money<TypedCurrency> result = chf.add(gbp); // exception!
Once currency is known, types are back:
Money<TypedCurrency> chfAmount = Money.from("100", TypedCurrency.from("CHF"));
Rate<CHF, USD> rate = Rate.from("1.29", TypedCurrency.CHF, TypedCurrency.USD);
// doesn't compile, unsafe
Money<USD> e1 = rate.exchange(chfAmount);
// compiles, runtime check
Money<USD> e2 = rate.exchangeOrThrow(chfAmount);
if (chfAmount.currency() instanceof CHF chf) {
// compiles, safe
Money<USD> e3 = rate.exchange(chfAmount.as(chf));
}