Jeder Marktplatz-Rewrite, den ich beobachtet habe, wurde von drei oder vier Entscheidungen aus den ersten sechs Wochen bezahlt — meist von Entscheidungen, über die niemand diskutiert hat, weil sie damals offensichtlich in Ordnung waren.
Das MVP selbst ist selten das Risiko. Das Risiko ist die Form, die es hinterlässt: die Tabelle, gegen die andere Teams anfangen zu joinen, die JSON-Spalte, die stillschweigend zum API-Vertrag wurde, der status-String, auf den inzwischen die halbe Geschäftslogik verzweigt. Das ist billig geschrieben und teuer verschoben, sobald Bestellungen, Rechnungen, Lieferanten und ein Finance-Team davon abhängen.
Was folgt, sind die Entscheidungen, die sich bezahlt gemacht haben, als ein Marktplatz von MVP und Go-live zu echtem Bestellvolumen, mehreren Lieferanten-Integrationen und einer Roadmap mit weiteren Ländern gewachsen ist. Keine davon ist exotisch. Alle sind am ersten Tag deutlich günstiger als im achtzehnten Monat.
Die Bestellung als Folge von Fakten modellieren
Der schnellste Weg, eine Bestellung zu modellieren, ist eine Zeile mit einer Status-Spalte. Das funktioniert bis zur ersten Rückerstattung. Dann bis zur Teillieferung. Dann kommt ein Lieferant, der zwei von drei Positionen versendet, und Finance fragt, wie die Bestellung am 14. ausgesehen hat.
Die günstige Variante von Event Sourcing — kein Framework, kein CQRS, kein separates Read Model — besteht darin, die Zeile zu behalten und die Fakten daneben anzuhängen:
type OrderEvent =
| { type: 'order.placed'; at: string; lines: OrderLine[]; total: Money }
| { type: 'payment.authorized'; at: string; amount: Money; providerRef: string }
| { type: 'payment.captured'; at: string; amount: Money; providerRef: string }
| { type: 'shipment.dispatched'; at: string; lineIds: string[]; carrier: Carrier }
| { type: 'refund.issued'; at: string; amount: Money; reason: RefundReason };Die orders-Zeile behält einen abgeleiteten Status, damit Abfragen und Listenansichten trivial bleiben. Die Event-Tabelle behält, warum dieser Status erreicht wurde. Event anhängen und abgeleitete Zeile in derselben Transaktion aktualisieren — das sind vielleicht zwei zusätzliche Tage im MVP. Dafür gilt:
- Abstimmung ist keine Engineering-Aufgabe mehr. Finance kann jede Bestellung zu jedem Zeitpunkt rekonstruieren, ohne jemanden um eine Query zu bitten.
- Der Support sieht einen Verlauf statt eines einzelnen Wortes, was eine ganze Kategorie von "Kannst du mal in der Datenbank schauen"-Anfragen erledigt.
- Debugging wird billig. Ein Fehler in der Statusableitung ist einen Replay davon entfernt, bestätigt zu sein, statt aus Logs zusammengeraten zu werden.
- Integrations-Handler werden append-only — und genau das macht sie sicher wiederholbar. Jede Integration wird irgendwann wiederholt.
Die Falle ist, das Event-Log zu einer zweiten, konkurrierenden Wahrheit mit eigenen, leicht abweichenden Regeln werden zu lassen. Ein Writer, eine Transaktion, eine Ableitungsfunktion, die in der Domänenschicht liegt und Tests hat. Wer nicht in einem Satz sagen kann, wie der Status abgeleitet wird, hat das Modell schon verloren.
Geld ist keine Zahl
Ein Feld price: number ist die Entscheidung, das Billing später neu zu schreiben. Zwei Dinge machen Geldbeträge schwierig, und keines davon ist Arithmetik: Geld hat immer eine Währung, und in großen Teilen Europas existiert jeder kundenseitige Betrag netto und brutto — erzeugt von einem Steuersatz, der sich ändern kann, während die Bestellungen weiterleben.
interface Money {
/** Kleinste Einheit. 1999 sind EUR 19,99. Niemals ein Float. */
amount: number;
currency: CurrencyCode;
}
interface LinePrice {
net: Money;
gross: Money;
taxRate: TaxRateId; // 'DE_19' | 'DE_7' | 'AT_20' | ...
taxAmount: Money;
}Drei Regeln tragen den größten Teil der Last:
- Beträge so speichern, wie sie berechnet wurden. Historische Summen nie aus einem aktuellen Steuersatz oder Katalogpreis neu berechnen. Die Bestellung ist die Aufzeichnung einer Vereinbarung, keine Projektion der heutigen Konfiguration.
- Den angewendeten Steuersatz persistieren, nicht eine Referenz, die auf den heute gültigen Satz auflöst. Sätze ändern sich, und Rechnungen von davor müssen weiterhin aufgehen.
- Einmal runden, auf einer dokumentierten Ebene. Pro Position oder pro Bestellung — eines auswählen, festhalten und den Grenzfall testen. Gemischtes Runden ist der Weg, auf dem ein Marktplatz bei hunderttausend Rechnungen einen Cent daneben liegt.
Die Währung gehört zur Bestellung, nicht zum Request. Sobald eine zweite Währung dazukommt, ist der beim Checkout verwendete Umrechnungskurs ein Fakt, der neben der Summe gespeichert gehört — sonst driften Rückerstattungen still gegen die ursprüngliche Zahlung, und es fällt erst auf, wenn ein Payout nicht mehr aufgeht.
Lieferanten sind eine Grenze, keine Tabelle
Die erste Lieferanten-Integration wird meist direkt ins Produktmodell geschrieben, weil es nur einen Lieferanten gibt und die Felder passen. Der zweite passt nicht, und nun hat das Produktmodell Felder, die nur für einen Partner etwas bedeuten.
Jeden Lieferanten als externes System hinter einer dünnen Übersetzungsschicht behandeln, die ausschließlich das kanonische Modell erzeugt:
- Das Rohformat behalten. Speichern, was der Lieferant tatsächlich geschickt hat, unveränderlich, neben dem normalisierten Ergebnis. Jeder Mapping-Fehler ist dann reproduzierbar, ohne den Partner um einen neuen Feed zu bitten.
- An der Grenze validieren. Irgendwann sendet ein Lieferant
"available": "yes", wo der Vertrag Boolean sagt. Am Rand mit einem Schema parsen —zodgenügt — und dort laut ablehnen, statt es drei Services später zu entdecken. - Das Mapping versionieren. Ändert sich eine Mapping-Regel, sollen alte Ingests sich weiterhin erklären können. Eine
mappingVersionam Ingest-Datensatz kostet eine Spalte. - Lieferanten-IDs nie in die öffentliche API lassen. Sobald eine Partner-SKU in einer URL oder einem Webhook auftaucht, wird ein Lieferantenwechsel zum Breaking Change für alle Konsumenten.
Das ist der Anti-Corruption Layer aus Domain-Driven Design und einer der wenigen DDD-Bausteine, die es sich lohnt anzuwenden, bevor der Schmerz da ist — denn die Alternative ist ein Domänenmodell in der Form desjenigen Partners, der zuerst integriert wurde.
Die Nahtstellen ziehen, die Internationalisierung braucht
Der teure Teil der Internationalisierung ist fast nie die Übersetzung. Teuer ist, dass Land, Währung, steuerliche Behandlung und rechtliche Einheit als Konstanten angenommen wurden und nun Parameter sind, die durch jeden preisführenden Codepfad laufen müssen.
- Locale ist keine Sprache.
de-ATteilt die Sprache mitde-DEund sonst kaum etwas, das beim Checkout zählt: andere Umsatzsteuer, andere Carrier, anderes Verbraucherrecht. - Das Paar explizit machen. Jeder Endpoint, der einen Preis zurückgibt, nimmt Land und Währung — auch solange beide genau einen möglichen Wert haben. Ein Parameter mit einer gültigen Option ist später eine Zeile Änderung; eine Annahme ist eine Migration.
- Die rechtliche Einheit in der Bestellung halten. Welches Unternehmen verkauft hat, bestimmt Rechnungslayout, steuerliche Meldung und Rücksendeadresse. Nachträglich abzuleiten ist Raten.
- Nichts lokalisieren, was keinen Verantwortlichen hat. Eine zweite Sprache, für deren Korrektheit niemand zuständig ist, schadet Vertrauen — und Sichtbarkeit in Suchmaschinen — mehr als eine einzige gut gepflegte.
Was sich in einem MVP nicht lohnt
Struktur ist billig, solange sie nur Struktur ist. Teuer wird sie, sobald sie Infrastruktur wird. Was ich früh aktiv vermeiden würde:
- Microservices. Ein modularer Monolith mit durchgesetzten Modulgrenzen lässt fast dieselbe Freiheit zum späteren Aufteilen, bei einem Bruchteil der Betriebskosten. Aufteilen, wenn ein Team oder ein Lastprofil es verlangt — nicht in Erwartung.
- Eine generische Rules Engine für Preise, Versand oder Promotions. Konfigurierbarkeit, die vor der dritten echten Regel entsteht, modelliert zuverlässig die falsche Achse.
- Mandantenfähigkeit vor dem zweiten Mandanten. Besonders im Datenmodell.
- Eigene Authentisierung. Hier gibt es keinen Vorteil, der das Risiko aufwiegt.
- Caching vor dem Messen. Frühe Caches verdecken meist die Query, die man hätte reparieren sollen, und desynchronisieren dann im Incident, der zählt.
Bestellungen als Fakten, Geld als typisierter Wert, Lieferanten hinter einer Grenze, Land und Währung als Parameter. Vier Entscheidungen, ein paar Wochen zusätzlicher Sorgfalt am Anfang — und das MVP darf das System bleiben, wenn das Volumen kommt, statt zu dem zu werden, das alle ersetzen wollen.



