Introduce optional bonus tasks in part4 covering: - CSV-based exchange rate provider - historical exchange rate provider with date handling - custom currency (DKP) with bidirectional conversion Provide exercise scaffolding and tests for all scenarios. Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
21 KiB
| title | author | theme | transition | class | drawings | comark | duration | exportFilename | subtitle | meta | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Beyond BigDecimal: Money and Currency API in Java (JSR-354) | Marcus Fihlon | default | null | text-center |
|
true | 180min | money-currency-api-slides | Slides for the Workshop | 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
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
BigDecimal total = price1.add(price2);
System.out.println(total); // 0.3
</span>
---
# 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
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
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
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→ ungenauBigDecimal→ nur eine ZahlCurrency→ 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 Leads: Anatole Tresch, Werner Keil und Otavio Santana
👉 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
Monetaryist die zentrale Factory der API- Bietet Zugriff auf Währungen und weitere Funktionen
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
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
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
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
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
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
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
- Geldbeträge und Währungen erzeugen
MoneyExercises
- Rechnen mit Geldbeträgen
ArithmeticExercises
- Rundung anwenden
RoundingExercises
- Formatierung
FormattingExercises
- Parsing
ParsingExercises
- 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
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
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
MonetaryAmountangewendet - Macht Fachlogik wiederverwendbar
- Hält Berechnung und Absicht zusammen
👉 Zum Beispiel: Rabatt, Steuer, Gebühren, Rundung
Fortgeschrittene Konzepte
Fachliche Operationen: Beispiel
MonetaryOperator discount = amount -> amount.multiply(0.9);
MonetaryAmount price = Money.of(100, "CHF");
MonetaryAmount reduced = price.with(discount);
- Ein
MonetaryOperatorverä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
MonetaryQuery<String> 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äzisionFastMoney→ 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
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
-
Währungsumrechnung
CurrencyConversionExercises -
Fachlogik kapseln
MonetaryOperatorExercises -
Informationen auslesen
MonetaryQueryExercises -
Kontext verstehen
MonetaryContextExercises -
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
Bonusaufgabe 1
Eigener Exchange Rate Provider
Package: swiss.fihlon.workshop.money.part4
- Implementiere einen eigenen
ExchangeRateProvider - Lade Wechselkurse aus
exchange-rates.csv - CSV enthält:
- Quellwährung
- Zielwährung
- Wechselkurs
- Stelle Umrechnungen zwischen den Währungen bereit
- Noch ohne Historisierung
Relevante Interfaces: ExchangeRateProvider
👉 Ziel: eigener Provider mit lokal geladenen Kursen
Bonus 2
Historische Kurse
Package: swiss.fihlon.workshop.money.part4
- Erweitere deinen Provider aus Bonus 1
- Verwende
exchange-rates-hist.csv - CSV enthält zusätzlich:
- Datum pro Wechselkurs
- Wähle den passenden Kurs für ein angefragtes Datum
- Fallback: sinnvoller Standard (z. B. letzter verfügbarer Kurs)
Relevante Interfaces: ExchangeRateProvider
👉 Ziel: zeitabhängige Wechselkurse unterstützen
Bonus 3
Eigene Währung
Package: swiss.fihlon.workshop.money.part4
- Implementiere die eigene Währung
DKP - Vorgegebener Umrechnungsfaktor:
- 10 CHF = 1 DKP
- Baue einen eigenen Converter
- Unterstütze:
- CHF → DKP
- DKP → CHF
Relevante Interfaces: CurrencyUnit und ExchangeRateProvider
👉 Ziel: eigene Währung ins Modell integrieren
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
- z. B.
- 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