money-currency-api-workshop/slides/slides.md

20 KiB
Raw Blame History

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
persist
false
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
```java BigDecimal price1 = new BigDecimal("0.1"); BigDecimal price2 = new BigDecimal("0.2");

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 → 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
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 1234.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 1234.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 1234.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 1234.56 de-CH gültig
1.234,56 CHF de-CH ungültig
CHF1,234.56 en-US gültig
CHF 1234.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 gehts weiter


Fortgeschrittene Konzepte

Weiter gehts

  • 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 MonetaryAmount angewendet
  • 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 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

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ä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

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!