---
title: "Beyond BigDecimal: Money and Currency API in Java (JSR-354)"
author: "Marcus Fihlon"
theme: default
transition: null
class: text-center
drawings:
persist: false
comark: true
duration: 180min
exportFilename: money-currency-api-slides
subtitle: Slides for the Workshop
meta: April 20th, 2026
---
# Beyond BigDecimal
## Money and Currency API in Java (JSR-354)
20\. April 2026
---
layout: two-cols
---
# Marcus Fihlon
::left::
Wer bin ich?
- Agile Coach und Scrum Master
- 40+ Jahre Software-Entwicklung
- 25+ Jahre im Java-Umfeld
- Organisator von Konferenzen
- Autor von Fachartikeln
- Vorträge und Workshops
- Open Source Committer
- Adventure Cyclist
::right::
---
# Workshop
Agenda
- Problem verstehen: Warum einfache Zahlen nicht reichen
- Grundlagen: Geldbeträge und Währungen korrekt modellieren
- Arbeiten mit Geldbeträgen: Rechnen, Rundung, Formatierung
**- Pause -**
- Fortgeschrittene Konzepte: Umrechnung, fachliche Operationen
- Architektur & Praxis: Einsatz im echten Code
---
# Das Problem
Geld ist keine Zahl
```java
double price1 = 0.1;
double price2 = 0.2;
double total = price1 + price2;
System.out.println(total);
```
- Erwartet:
0.3
- Ergebnis:
0.30000000000000004
- Grund: binäre Gleitkommazahlen
```java
BigDecimal price1 = new BigDecimal("0.1");
BigDecimal price2 = new BigDecimal("0.2");
BigDecimal total = price1.add(price2);
System.out.println(total); // 0.3
```
---
# Das Problem
Währung ist nicht optional
```java
BigDecimal price = new BigDecimal("10.00");
BigDecimal tax = new BigDecimal("0.77");
BigDecimal total = price.add(tax);
```
- CHF? EUR? USD?
- Wie viele Nachkommastellen?
- Unterschiedliche Rundung je nach Währung
- Fehler entstehen nicht im Code, sondern im Modell
---
# Das Problem
`Currency` ist eingeschränkt
```java
Currency currency = Currency.getInstance("CHF");
```
- Nur ISO-4217 Währungen
- Neue Währungen fehlen oft initial
- Keine historischen Währungen
- Keine Unterstützung für Kryptowährungen
- Keine projektspezifischen oder virtuellen Währungen
- Keine Kontextabhängigkeit, z. B. CHF:
- elektronische Zahlung: 2 Nachkommastellen
- Barzahlung: Rundung auf 0.05
---
# Das Problem
Nachkommastellen sind nicht einheitlich
```java
BigDecimal amount = new BigDecimal("10.123") // gültig oder nicht?
```
- CHF: 2 Nachkommastellen
- JPY: keine Nachkommastellen
- TND: 3 Nachkommastellen
---
# Das Problem
Rundung ist fachlich, nicht technisch
```java
BigDecimal value = new BigDecimal("10.005");
value.setScale(2, RoundingMode.HALF_UP); // 10.01
value.setScale(2, RoundingMode.HALF_DOWN); // 10.00
```
**Welche Regel ist korrekt?**
Das kommt auf den fachlichen Kontext an!
---
# Das Problem
Zusammengefasst
**Falsche Abstraktion**
- `double` → ungenau
- `BigDecimal` → nur eine Zahl
- `Currency` → zu wenig Kontext
**Was fehlt**
- Betrag und Währung gehören zusammen
- Rundung ist Teil der Fachlogik
- Währungsregeln sind nicht universell
- Es fehlen wichtige Metadaten
👉 Geld wird nicht als Domänenkonzept modelliert
---
# Grundlagen
Die Lösung: JSR-354
**Money and Currency API for Java**
- Fachlich modelliert, nicht technisch
- Geld als Domänenmodell statt primitiver Typen
- Betrag und Währung als Einheit
- Fachlich korrekte Operationen und Rundung
- Erweiterbar für eigene Anforderungen
👉 Weg von `double`, `BigDecimal` und `Currency` als Einzelteile
👉 Hin zu einem konsistenten Modell für Geldbeträge
---
# Grundlagen
Was ist JSR-354?
**Money and Currency API for Java**
- Teil der Java-Spezifikation (JSR = Java Specification Request)
- Standardisierte API für Geldbeträge und Währungen
- Trennt API und Implementierung
**Wer steckt dahinter?**
- Spezifikation: Java Community Process (JCP)
- Referenzimplementierung: Moneta
- Entwickelt und gepflegt von der Java-Community
- Spec Lead JSR-354: Werner Keil
- Lead der Referenzimplementierung (Moneta): Anatole Tresch
👉 Open Source und gemeinschaftlich weiterentwickelt
---
# Grundlagen
Der zentrale Typ
**MonetaryAmount**
- Repräsentiert einen Geldbetrag
- Kombiniert Wert und Währung
- Definiert fachliche Operationen
**Warum ein Interface?**
- Trennung von API und Implementierung
- Austauschbare Implementierungen
- Anpassbar an unterschiedliche Anforderungen
- Präzision vs. Performance
- Eigene Implementierungen möglich
👉 Fokus auf Modell, nicht auf Implementierungen
---
# Grundlagen
Die Währung
**CurrencyUnit**
- Repräsentiert eine Währung
- Teil des Geldbetrags
- Eindeutig über Code (z. B. CHF, EUR)
- Liefert grundlegende Metadaten
**Warum ein Interface?**
- Trennung von API und Implementierung
- Erweiterbar über ISO-4217 hinaus
- Unterstützung für neue und virtuelle Währungen
- Einheitliches Modell für alle Währungen
👉 Währungen sind Teil des Modells, nicht nur ein Zusatz
---
# Grundlagen
Die Standard-Implementierung
**Money**
- Implementiert `MonetaryAmount`
- Repräsentiert einen konkreten Geldbetrag
- Basierend auf `BigDecimal`
- Hohe Präzision für fachlich korrekte Berechnungen
**Warum diese Implementierung?**
- Präzise und verlässlich
- Geeignet für die meisten Anwendungsfälle
- Einfache Verwendung im Code
👉 Einstiegspunkt für die praktische Nutzung der API
---
# Grundlagen
Ein erstes Beispiel
- `Monetary` ist die zentrale Factory der API
- Bietet Zugriff auf Währungen und weitere Funktionen
```java
CurrencyUnit chf = Monetary.getCurrency("CHF");
MonetaryAmount amount = Money.of(19.95, chf);
System.out.println(amount);
System.out.println(amount.getNumber());
System.out.println(amount.getCurrency().getCurrencyCode());
```
Ausgabe:
```
CHF 19.95
19.95
CHF
```
---
# Grundlagen
Arithmetische Operationen
```java
CurrencyUnit chf = Monetary.getCurrency("CHF");
MonetaryAmount price = Money.of(19.95, chf);
MonetaryAmount shipping = Money.of(4.95, chf);
MonetaryAmount total = price.add(shipping);
System.out.println(total); // CHF 24.90
```
- Operationen sind direkt auf Geldbeträgen möglich
- Währung wird automatisch berücksichtigt
- Ergebnis bleibt ein Geldbetrag
---
# Grundlagen
Weitere Operationen
```java
MonetaryAmount price = Money.of(19.95, chf);
MonetaryAmount shipping = Money.of(4.95, chf);
MonetaryAmount discount = Money.of(10, chf);
MonetaryAmount total =
price
.add(shipping)
.subtract(discount)
.multiply(2)
.divide(3);
```
- Fluent API für gut lesbaren Code
- Operationen bleiben im Geldmodell
- Ergebnis ist immer wieder ein Geldbetrag
---
# Grundlagen
Rundung
```java
MonetaryAmount amount = Money.of(10, "CHF");
MonetaryAmount result = amount.divide(3);
System.out.println(result); // ?
```
- Ergebnis nicht exakt darstellbar
- Rundung notwendig
- Wie wird gerundet?
---
# Grundlagen
Rundung ist fachlich
- Keine „technische“ Entscheidung
- Abhängig von:
- Währung
- Anwendungsfall
- Geschäftsregeln
👉 Rundung ist Teil der Fachlogik
---
# Grundlagen
Rundung mit der API
```java
MonetaryAmount amount = Money.of(10, "CHF");
MonetaryAmount result =
amount
.divide(3)
.with(Monetary.getDefaultRounding());
System.out.println(result); // CHF 3.33
```
- Rundung erfolgt explizit
- Standard-Rundung abhängig von der Währung
- Rundung ist eine externe Fachregel (Operator)
- Wird bewusst auf den Geldbetrag angewendet
👉 Rundung ist kein Nebeneffekt, sondern Teil der Fachlogik
---
# Grundlagen
Formatierung
```java
MonetaryAmount amount = Money.of(1234.56, "CHF");
Locale locale = Locale.forLanguageTag("de-CH");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locale);
String formatted = format.format(amount);
System.out.println(formatted); // CHF 1’234.56
```
- Formatierung abhängig von Locale
- Währung wird korrekt dargestellt
- Kein manuelles String-Handling
👉 Darstellung ist kontextabhängig, nicht Teil des Betrags
---
# Grundlagen
Formatierung nach Locale
| **Locale** | **Ausgabe** |
|---------------|------------------|
| Deutschland | 1.234,56 CHF |
| USA | CHF1,234.56 |
| Schweiz (de) | CHF 1’234.56 |
- Gleicher Betrag, unterschiedliche Darstellung
- Formatierung ist kontextabhängig
---
# Grundlagen
Parsen
```java
Locale locale = Locale.forLanguageTag("de-CH");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locale);
MonetaryAmount amount = format.parse("CHF 1’234.56");
System.out.println(amount); // CHF 1234.56
```
- Parsing ist abhängig von Locale und Format
- Währung und Betrag werden gemeinsam eingelesen
- Kein manuelles Zerlegen von Strings
👉 Auch das Einlesen ist kontextabhängig
---
# Grundlagen
Einlesen nach Locale
| **Eingabe** | **Locale** | **Ergebnis** |
|-----------------|------------|--------------|
| CHF 1’234.56 | de-CH | gültig |
| 1.234,56 CHF | de-CH | ungültig |
| CHF1,234.56 | en-US | gültig |
| CHF 1’234.56 | en-US | ungültig |
- Parsing ist strikt formatabhängig
- Falsches Locale: Fehler oder falsches Ergebnis
---
# Grundlagen
Was haben wir gelernt?
- Geldbeträge korrekt modellieren (Betrag + Währung)
- Mit Geldbeträgen rechnen statt mit Zahlen
- Rundung als fachliche Entscheidung anwenden
- Geldbeträge formatieren und einlesen
- Kontext (Locale, Regeln) bewusst berücksichtigen
👉 Jetzt seid ihr dran: ab in die Praxis
---
# Übungen (Teil 1)
Empfohlene Reihenfolge
1. Geldbeträge und Währungen erzeugen
- `MoneyExercises`
2. Rechnen mit Geldbeträgen
- `ArithmeticExercises`
3. Rundung anwenden
- `RoundingExercises`
4. Formatierung
- `FormattingExercises`
5. Parsing
- `ParsingExercises`
6. Alles zusammenführen
- `IntegrationExercises`
---
# Pause ☕
30 Minuten
- Kurz durchatmen
- Beine vertreten
- Kaffee holen
👉 In 30 Minuten geht’s weiter
---
# Fortgeschrittene Konzepte
Weiter geht’s
- Zurück in den Flow
- Jetzt wird es interessanter
- Wir gehen einen Schritt tiefer
**Was schauen wir uns an?**
- Währungsumrechnung
- Wiederverwendbare Operationen
- Unterschiedliche Rundungsstrategien
👉 Mehr Möglichkeiten als in den Grundlagen
---
# Fortgeschrittene Konzepte
Währungsumrechnung: Herausforderungen
- Wechselkurse sind nicht stabil
- Zeitpunkt der Umrechnung ist relevant
- Rundung beeinflusst das Ergebnis
- Richtung der Umrechnung macht einen Unterschied
- Datenquelle muss vertrauenswürdig sein
👉 Umrechnung ist mehr als eine einfache Berechnung
---
# Fortgeschrittene Konzepte
Währungsumrechnung: Lösung
```java
MonetaryAmount amountCHF = Money.of(10, "CHF");
CurrencyConversion conversion = MonetaryConversions.getConversion("EUR", "ECB");
MonetaryAmount amountEUR = amountCHF.with(conversion);
System.out.println(amountEUR); // EUR 10.83306250677066
```
- Umrechnung ist eine eigene Operation
- Wechselkurs wird extern bereitgestellt
- Ergebnis bleibt ein Geldbetrag
---
# Fortgeschrittene Konzepte
Währungsumrechnung: Wechselkursquelle
- Der Geldbetrag kennt keinen Wechselkurs
- Der Wechselkurs kommt von einem Provider
- Beispiele verfügbarer Provider:
- `ECB` – European Central Bank (EZB)
- `IMF` – International Monetary Fund (IWF)
- Unterschiedliche Provider liefern unterschiedliche Daten
- Ohne Provider gibt es keine Umrechnung
👉 Der Wechselkurs ist externer Kontext, nicht Teil des Betrags
---
# Fortgeschrittene Konzepte
Währungsumrechnung: Fallstricke
- Unterschiedliche Kurse je nach Zeitpunkt
- Unterschiedliche Ergebnisse je nach Datenquelle
- Rundung beeinflusst das Ergebnis
- Mehrere Umrechnungen verstärken Abweichungen
👉 Es gibt nicht das eine „richtige“ Ergebnis
---
# Fortgeschrittene Konzepte
Währungsumrechnung: Zeitpunkt
| **Zeitpunkt** | **Kurs (CHF → EUR)** | **Ergebnis** |
|:--------------|---------------------:|--------------:|
| Gestern | 1.08 | EUR 10.80 |
| Heute | 1.10 | EUR 11.00 |
- Gleicher Betrag, unterschiedliches Ergebnis
- Wechselkurse sind Momentaufnahmen
---
# Fortgeschrittene Konzepte
Währungsumrechnung: Zeitpunkt im Code
```java
MonetaryAmount amountCHF = Money.of(10, "CHF");
ConversionQuery query = ConversionQueryBuilder.of()
.setTermCurrency("EUR")
.set(LocalDate.class, LocalDate.of(2026, 3, 13))
.build();
CurrencyConversion conversion = MonetaryConversions.getConversion(query);
MonetaryAmount amountEUR = amountCHF.with(conversion);
```
- Zeitpunkt ist Teil der Anfrage
- Provider bestimmt den passenden Kurs
- Nicht jeder Provider unterstützt Zeitpunkte
- Manche liefern nur Tageskurse, manche ignorieren den Zeitpunkt
👉 Ergebnis hängt vom Provider ab
---
# Fortgeschrittene Konzepte
Fachliche Operationen
**MonetaryOperator**
- Kapselt fachliche Operationen auf Geldbeträgen
- Wird auf einen `MonetaryAmount` angewendet
- Macht Fachlogik wiederverwendbar
- Hält Berechnung und Absicht zusammen
👉 Zum Beispiel: Rabatt, Steuer, Gebühren, Rundung
---
# Fortgeschrittene Konzepte
Fachliche Operationen: Beispiel
```java
MonetaryOperator discount = amount -> amount.multiply(0.9);
MonetaryAmount price = Money.of(100, "CHF");
MonetaryAmount reduced = price.with(discount);
```
- Ein `MonetaryOperator` verändert einen Geldbetrag
- Anwendung mit `with(...)`
- Fachlogik wird benennbar und wiederverwendbar
- Rabattlogik nicht mehrfach schreiben
- Steuerberechnung zentral kapseln
- Rundungsregeln wiederverwenden
---
# Fortgeschrittene Konzepte
Informationen gezielt auslesen
**MonetaryQuery**
- Liest gezielt Informationen aus einem Geldbetrag
- Verändert den Geldbetrag nicht
- Kapselt wiederverwendbare Ausleselogik
👉 Das Gegenstück zum `MonetaryOperator`
---
# Fortgeschrittene Konzepte
Informationen gezielt auslesen: Beispiel
```java
MonetaryQuery currencyCode =
amount -> amount.getCurrency().getCurrencyCode();
MonetaryAmount amount = Money.of(100, "CHF");
String code = amount.query(currencyCode);
```
- Anwendung mit `query(...)`
- Ergebnis ist frei wählbar
- Fachliche Ausleselogik wird wiederverwendbar
---
# Fortgeschrittene Konzepte
Der Kontext entscheidet
**Was beeinflusst eigentlich einen Geldbetrag?**
- Wie viele Nachkommastellen sind erlaubt?
- Wie präzise wird gerechnet?
- Welche Rundung wird verwendet?
👉 Diese Eigenschaften gehören nicht zum Wert selbst
---
# Fortgeschrittene Konzepte
Der Kontext entscheidet
**MonetaryContext**
- Beschreibt Eigenschaften eines Geldbetrags
- Zum Beispiel:
- Präzision
- maximale Nachkommastellen
- Wird von Implementierungen verwendet
👉 Der Kontext bestimmt, wie gerechnet wird
---
# Fortgeschrittene Konzepte
Der Kontext entscheidet
**MonetaryContext**
- Ein Teil ist für alle gleich
- Implementierungen können den Kontext erweitern
- Unterschiedliche Implementierungen können sich unterschiedlich Verhalten
- Beispiel:
- `Money` → hohe Präzision
- `FastMoney` → feste Skalierung, begrenzte Präzision
👉 Gleicher Wert, unterschiedliches Verhalten
---
# Fortgeschrittene Konzepte
Rundungsstrategien
- Anzeige für den Benutzer
- Berechnung von Steuern
- Barzahlung (z. B. CHF 0.05)
- Interne Berechnungen
**Das bedeutet**
- Rundung ist eine fachliche Entscheidung
- Kann je Use Case unterschiedlich sein
- Wird explizit angewendet
👉 Unterschiedliche Situationen, unterschiedliche Regeln
---
# Fortgeschrittene Konzepte
Rundungsstrategien im Code
```java
MonetaryOperator vat = amount -> amount.multiply(0.077);
MonetaryOperator rounding = Monetary.getDefaultRounding();
MonetaryAmount result =
amount
.with(vat)
.with(rounding);
```
- Fachlogik wird kapselbar
- Rundung ist ein eigener Schritt
- Operatoren lassen sich kombinieren
---
# Fortgeschrittene Konzepte
Was haben wir gesehen?
- Währungsumrechnung ist kontextabhängig
- Fachlogik lässt sich kapseln (`MonetaryOperator`)
- Informationen gezielt auslesen (`MonetaryQuery`)
- Kontext beeinflusst das Verhalten (`MonetaryContext`)
- Rundung ist eine bewusste Entscheidung
👉 Jetzt seid ihr dran: ab in die Praxis
---
# Übungen (Teil 2)
Empfohlene Reihenfolge
1. Währungsumrechnung
`CurrencyConversionExercises`
2. Fachlogik kapseln
`MonetaryOperatorExercises`
3. Informationen auslesen
`MonetaryQueryExercises`
4. Kontext verstehen
`MonetaryContextExercises`
5. Rundungsstrategien anwenden
`RoundingStrategyExercises`
---
# Architektur & Praxisintegration
Worum geht es jetzt?
- Geldbeträge im Domänenmodell
- Persistenz und Mapping
- Fachlogik an der richtigen Stelle
- Übergang von API-Wissen zu Anwendungsarchitektur
---
# Architektur & Praxisintegration
Geld gehört ins Domänenmodell
- Geld ist kein `double`
- Geld ist kein `BigDecimal`
- Geld ist kein Paar aus Zahl und String
👉 Geld ist ein eigener fachlicher Typ
---
# Architektur & Praxisintegration
Wo gehört die Logik hin?
- Nicht in die UI
- Nicht in SQL
- Nicht verstreut in Utility-Klassen
**Sondern:**
- ins Domänenmodell
- in fachliche Services
- in klar benannte Operationen
---
# Architektur & Praxisintegration
Persistenz
- Datenbank kennt meist keinen `MonetaryAmount`
- Betrag und Währung müssen gespeichert werden
- Mapping ist eine bewusste Entscheidung
👉 Fachmodell und Persistenzmodell sind nicht dasselbe
---
# Architektur & Praxisintegration
Typische Mapping-Strategien
**Zwei Spalten:**
- Betrag
- Währung
**Optional:**
- Embeddable / Value Object
- Converter für Persistenz
👉 Wichtig ist Konsistenz
---
# Architektur & Praxisintegration
Was ist eine gute Lösung?
- Fachlich korrekt
- Im Code lesbar
- Persistierbar
- Testbar
- Ohne versteckte Logik
---
# Übungen (Teil 3)
Warenkorb / Bestellung
- Zwischensumme berechnen
- Rabatt anwenden
- Steuer berechnen
- Gesamtbetrag bestimmen
- Ergebnis formatieren
👉 Setzt die Schritte nacheinander um
👉 Verwendet die bisherigen Konzepte bewusst
**Zusatzaufgabe**
- Gesamtbetrag zusätzlich in EUR anzeigen
- Währungsumrechnung verwenden
👉 Optional für schnelle Teilnehmer
---
# Voraussetzungen
Java-Versionen
- JSR-354 läuft auf modernen Java-Versionen
- Typischerweise:
- Java 11+
- Java 17 (LTS)
- Java 21 (LTS)
- Wichtig:
- Einheitliche Version im Team
- Gleiche Version für Workshop und Übungen
👉 Unterschiedliche Versionen können Verhalten beeinflussen
---
# Voraussetzungen
Dependencies
- Basis:
- `moneta-core`
- Für Währungsumrechnung zusätzlich nötig:
- z. B. `moneta-convert-ecb`, `moneta-convert-imf`
- oder andere Provider
- Wichtig:
- Ohne Provider keine Currency Conversion
- Verhalten hängt vom Provider ab
👉 Dependencies bewusst wählen
---
# Wrap-up
Was bleibt hängen?
- Geld ist ein Domänenkonzept
- Rundung ist Fachlogik
- Kontext beeinflusst Verhalten
- Umrechnung braucht externe Daten
- Die API kann mehr als nur rechnen
---
# Wrap-up
Worauf kommt es in der Praxis an?
- Fachmodell vor Technik
- Klare Verantwortung im Code
- Explizite Rundung
- Konsistentes Mapping
- Wiederverwendbare Logik
---
layout: center
---
# Fragen?
---
layout: center
---
# **Danke!**