costly/costly-api/README.md
Bastian Wagner e05ab13d0d group events
2025-12-19 13:14:04 +01:00

194 lines
4.6 KiB
Markdown
Raw Permalink 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.

# Costly Architektur & Entscheidungsgrundlagen
## Ziel des Projekts
Costly ist eine **Offline-fähige PWA zur Aufteilung von Gruppenausgaben**.
Kernanforderungen:
- Gruppen ohne Accounts (Join per Invite)
- Offline Änderungen möglich
- Späterer Sync ohne Datenverlust
- Erweiterbar für Konflikte, Undo, Mehrgeräte-Nutzung
---
## Architektur Überblick
Costly nutzt eine **Hybrid Event-Driven Architektur**:
- **Events sind die Quelle der Wahrheit**
- **Relationale Tabellen sind Read-Models**
- Offline-Sync basiert auf **append-only Events**
### Überblick
Client
├─ Offline Outbox (Events)
├─ UI arbeitet mit aktuellem State
└─ Sync: sendet Events + holt neue Events
Server
├─ Event Store (group_events, expense_events, ...)
├─ Read Models (groups, expenses, ...)
└─ Projektion: Events → State
yaml
Code kopieren
---
## Warum Events?
### Nicht: „Wie sieht der Zustand aus?“
### Sondern: „Was ist passiert?“
Beispiel:
-`Group.name = "Trip"`
-`GROUP_RENAMED { from: "Urlaub", to: "Trip" }`
### Vorteile
- Offline-Änderungen lassen sich sicher synchronisieren
- Events sind idempotent (keine Doppeländerungen)
- Konflikte sind sichtbar und erklärbar
- Undo / Replay später möglich
- Debugging & Audit inklusive
---
## Event-Log vs. History
- `group_events` **ersetzt** klassische `group_history`
- Keine doppelte Speicherung
- Events *sind* die History
Merksatz:
> **History sagt, was ist.
> Events sagen, was passiert ist.**
---
## Hybrid-Ansatz (bewusst gewählt)
Costly ist **kein dogmatisches Event Sourcing**.
Wir nutzen:
- ✅ Events als Quelle der Wahrheit
- ✅ Materialisierte Tabellen (`groups`, `expenses`) für schnelle Queries
➡️ Best of both worlds.
---
## Aktueller Konflikt-Ansatz
**Last-write-wins**, basierend auf Server-Reihenfolge der Events.
- Client, der später synchronisiert, kann frühere Änderungen überschreiben
- Das ist **bewusst akzeptiert** fürs MVP
Spätere Erweiterung möglich (siehe unten).
---
## Event-Grundstruktur
```ts
GroupEvent {
id: string; // vom Client generiert (UUID/ULID)
groupId: string;
type: string; // z.B. GROUP_CREATED, GROUP_RENAMED
payload: object; // fachliche Daten
actorId?: string; // deviceId / memberId
createdAt: Date; // Server-Zeit
}
```
## Warum Client-generierte IDs?
### Ermöglicht Idempotenz
- Events können gefahrlos erneut gesendet werden
- Wichtig für Offline-Retry
### Warum es weiterhin REST-Endpunkte gibt
Es gibt z. B.:
- PATCH /groups/:id/name
Diese Endpunkte:
- validieren Input
- prüfen Regeln
- erzeugen intern Events
Der Client muss Events nicht kennen, solange er online ist.
➡️ Später können Offline-Events direkt an /events/batch geschickt werden.
## Transaktionen aktueller Stand
Jede fachliche Aktion läuft atomar:
- Event wird gespeichert
- Read Model wird aktualisiert
- Beides in einer DB-Transaktion
Beispiel:
```sql
Code kopieren
renameGroup()
├─ UPDATE groups
└─ INSERT group_events
```
### Erweiterung: Optimistic Concurrency (später)
Problem:
- Zwei Geräte ändern offline denselben Wert
Lösung:
- Client sendet zusätzlich expectedLastEventId
- Server prüft:
- passt → akzeptieren
- passt nicht → 409 Conflict
Das ist ohne Architekturwechsel möglich, weil Events genutzt werden.
## Erweiterung: Transaktionen als Event-Gruppe
Wenn später komplexe Änderungen kommen (z. B. mehrere Expenses auf einmal):
Idee
Mehrere Events gehören logisch zusammen.
```ts
{
transactionId: "tx-123",
type: "EXPENSE_CREATED"
}
{
transactionId: "tx-123",
type: "EXPENSE_SPLIT_UPDATED"
}
```
### Möglichkeiten:
Server behandelt Events mit gleicher transactionId atomar
- Undo / Rollback pro Transaktion möglich
- UI kann „eine Aktion“ anzeigen statt viele Events
### Erweiterung: Undo / Replay
Da Events append-only sind:
- Undo = Gegen-Event
- Replay = Events neu anwenden → State neu aufbauen
Beispiel:
- GROUP_RENAMED (A → B)
- Undo → GROUP_RENAMED (B → A)
Wichtige Designregeln
- Repositories kennen keine Events
- Services orchestrieren State + Event
- Events sind append-only
- Read Models dürfen neu aufgebaut werden
- Keine Logik im Controller
Warum das alles?
Weil Costly:
- offline funktionieren soll
- ohne Accounts auskommen soll
- später wachsen können soll
- aber jetzt schon stabil sein muss
Diese Architektur ist der kleinste sinnvolle Schritt, um das zu erreichen.
## Status
- [x]Groups als Events
- [x] Rename als Event
- [ ] Expenses als Events
- [ ] Client-Outbox (IndexedDB)
- [ ] Sync Pull (GET /events?since=...)
- [ ] Conflict Handling (optional)