--- 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); ```
```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); ```
--- # 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 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 - `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!**