money-currency-api-workshop/slides/slides.md
Marcus Fihlon 50b6605285
fix(slides): correct date type in historical exchange rate example
Use `LocalDate` instead of `LocalDateTime` to align with
Moneta/ECB-HIST expectations for date-based queries.
2026-04-20 00:19:32 +02:00

1061 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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
<img src="https://www.fihlon.swiss/qr-code.png" class="mt-4" style="width: auto; height: auto; max-width: none; image-rendering: pixelated;" />
::right::
<img src="https://www.fihlon.swiss/portrait.webp" class="w-3/4 mt-8" />
---
# Workshop
Agenda
- Problem verstehen: Warum einfache Zahlen nicht reichen
- Grundlagen: Geldbeträge und Währungen korrekt modellieren
- Arbeiten mit Geldbeträgen: Rechnen, Rundung, Formatierung
<div class="my-6"></div>
**- Pause -**
<div class="my-6"></div>
- 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);
```
<div class="my-6"></div>
<ul v-click>
<li>Erwartet: <code>0.3</code></li>
<li>Ergebnis: <code>0.30000000000000004</code></li>
<li>Grund: binäre Gleitkommazahlen</li>
</ul>
<div class="my-6"></div>
<span v-click>
```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);
```
<div class="my-6"></div>
<ul v-click>
<li>CHF? EUR? USD?</li>
<li>Wie viele Nachkommastellen?</li>
<li>Unterschiedliche Rundung je nach Währung</li>
<li>Fehler entstehen nicht im Code, sondern im Modell</li>
</ul>
---
# Das Problem
`Currency` ist eingeschränkt
```java
Currency currency = Currency.getInstance("CHF");
```
<div class="my-6"></div>
- 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?
```
<div class="my-6"></div>
- 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
```
<div class="my-6"></div>
**Welche Regel ist korrekt?**
<span v-click>
Das kommt auf den fachlichen Kontext an!
</span>
---
# 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
```
<div class="my-6"></div>
- 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);
```
<div class="my-6"></div>
- 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); // ?
```
<div class="my-6"></div>
- 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
```
<div class="my-6"></div>
- 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 1234.56
```
<div class="my-6"></div>
- 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 |
<div class="my-6"></div>
- 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 1234.56");
System.out.println(amount); // CHF 1234.56
```
<div class="my-6"></div>
- 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 |
<div class="my-6"></div>
- 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
```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
```
<div class="my-6"></div>
- 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 |
<div class="my-6"></div>
- 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);
```
<div class="my-6"></div>
- 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);
```
<div class="my-6"></div>
- 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<String> currencyCode =
amount -> amount.getCurrency().getCurrencyCode();
MonetaryAmount amount = Money.of(100, "CHF");
String code = amount.query(currencyCode);
```
<div class="my-6"></div>
- 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);
```
<div class="my-6"></div>
- 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!**