import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
import java.util.Objects;
public final class Money {
private final BigDecimal amount;
private final Currency currency;
private Money(BigDecimal amount, Currency currency) {
int scale = currency.getDefaultFractionDigits();
this.amount = amount.setScale(scale, RoundingMode.UNNECESSARY);
this.currency = currency;
}
static Money create(BigDecimal amount, Currency currency) {
return new Money(amount, currency);
}
public static MoneyBuilder builder() {
return new MoneyBuilder();
}
public BigDecimal amount() {
return amount;
}
public Currency currency() {
return currency;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money other = (Money) o;
return amount.equals(other.amount) && currency.equals(other.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
@Override
public String toString() {
return amount.toPlainString() + " " + currency.getCurrencyCode();
}
}
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Currency;
import java.util.List;
public final class MoneyBuilder {
private BigDecimal amount;
private Currency currency;
MoneyBuilder() {
}
public MoneyBuilder amount(BigDecimal amount) {
this.amount = amount;
return this;
}
public MoneyBuilder amount(String amount) {
this.amount = amount == null ? null : new BigDecimal(amount);
return this;
}
public MoneyBuilder currency(String isoCode) {
this.currency = isoCode == null ? null : Currency.getInstance(isoCode);
return this;
}
public Money build() {
List<String> errors = new ArrayList<>();
require(amount != null, errors, "amount is required");
require(currency != null, errors, "currency is required");
if (amount != null) {
require(amount.signum() >= 0, errors, "amount must not be negative");
if (currency != null) {
int digits = currency.getDefaultFractionDigits();
require(amount.scale() <= digits, errors,
"amount has more decimals than " + currency.getCurrencyCode() + " allows");
}
}
if (!errors.isEmpty()) {
throw new IllegalStateException("Invalid Money: " + String.join("; ", errors));
}
return Money.create(amount, currency);
}
private static void require(boolean condition, List<String> errors, String message) {
if (!condition) {
errors.add(message);
}
}
}
import java.math.BigDecimal;
public class MoneyBuilderUsage {
public static void main(String[] args) {
Money price = Money.builder()
.amount("19.99")
.currency("USD")
.build();
System.out.println(price); // 19.99 USD
System.out.println(price.amount()); // 19.99
Money copy = Money.builder()
.amount(new BigDecimal("19.99"))
.currency("USD")
.build();
System.out.println(price.equals(copy)); // true
try {
Money.builder()
.amount("-1.234")
.build();
} catch (IllegalStateException e) {
// Invalid Money: currency is required; amount must not be negative
System.out.println(e.getMessage());
}
}
}
This snippet shows the classic combination for modeling a domain value in Java: an immutable object plus a fluent builder that validates everything in one place at build() time. The Money value object is the target type, and the MoneyBuilder usage tab exercises the API the way calling code actually reads.
The Money value object is a final class with final fields and no setters, so once constructed an instance can never change. That immutability is the reason such objects are safe to share across threads and cache freely — there is no observable state transition to guard. The private constructor forces all creation through the builder, which keeps the invariants in a single choke point. Note the defensive handling of Currency and amount: the class normalizes the scale to the currency's fraction digits so two equal amounts always compare and hash equally, which is what makes equals and hashCode reliable for use in sets and map keys.
The MoneyBuilder is a static nested class returning this from each setter, giving the fluent chain. Crucially, none of the setters throw; they merely record intent. All validation is deferred to build(), where require checks accumulate into a single IllegalStateException listing every problem at once instead of failing on the first bad field. This is friendlier for callers than a scatter of guard clauses and mirrors how bean-validation frameworks report multiple violations together.
The pattern solves the telescoping-constructor problem: instead of many overloaded constructors with ambiguous positional arguments, named setters make each value explicit and optional fields easy. The trade-off is more boilerplate and a transient mutable builder, so it pays off mainly for objects with several fields or non-trivial invariants — a two-field struct rarely needs it.
The MoneyBuilder usage tab demonstrates both the happy path and how the aggregated error message surfaces. A subtle pitfall worth noting: because the builder is mutable and not thread-safe, one builder instance should not be shared across threads, and build() should be treated as producing an independent snapshot. Reusing a builder to produce several objects is fine as long as it stays confined to one thread.
Related snips
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency = 'USD')
Value objects for domain modeling
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
Laravel form requests for validation
class SubscriptionsController < ApplicationController
def new
@subscription = current_account.subscriptions.new
end
def create
Turbo Streams: append server-side validation warnings
import { z } from "zod";
const booleanFromString = z.preprocess((val) => {
if (typeof val !== "string") return val;
return ["true", "1", "yes", "on"].includes(val.toLowerCase());
}, z.boolean());
Typed env parsing with zod
Share this code
Here's the card — post it anywhere.