Der erste vertikale Slice
此内容尚不支持你的语言。
Nach den bisherigen Infrastruktur-Schritten ist die Turnier-App technisch startklar.
Die Anwendung läuft. Das Deployment steht. Die CI prüft statische Quality Gates. Keycloak ist integriert. Das API Gateway validiert JWTs. Transloco, Tailwind und PrimeNG bilden die Grundlage für UI und Texte.
Damit ist der Punkt erreicht, an dem das Projekt endlich fachlich werden darf.
Und genau hier lauert die nächste Falle.
Denn der erste fachliche Schritt entscheidet oft darüber, wie sich ein Projekt weiterentwickelt. Wenn man jetzt einfach “Spielerverwaltung bauen” sagt, entsteht schnell Code. Vielleicht sogar funktionierender Code. Aber nicht zwingend eine tragfähige Struktur.
Deshalb geht es in diesem Schritt nicht nur um CRUD.
Es geht um den ersten vertikalen Slice.
Warum nicht einfach irgendwo anfangen?
Abschnitt betitelt „Warum nicht einfach irgendwo anfangen?“Die fachliche Idee der App ist überschaubar: Ein Verein möchte Tischtennisturniere vorbereiten und durchführen. Dafür braucht es Spieler, Turniere, Gruppen, Spielpläne, Ergebnisse und Historie.
Man könnte also direkt mit irgendeinem sichtbaren Feature starten.
Zum Beispiel:
Spieler anlegenTurnier erstellenGruppen per Drag & Drop einteilenSpielplan generierenDer sichtbare Reiz liegt wahrscheinlich bei der Gruppeneinteilung. Drag & Drop, Gruppen, Ranking, Turniertag — das fühlt sich nach Produkt an.
Trotzdem ist das nicht der beste erste Schritt.
Die Spieler-Verwaltung ist kleiner. Langweiliger. Weniger beeindruckend.
Aber genau deshalb eignet sie sich gut als erster fachlicher Durchstich.
Sie ist fachlich einfach genug, um nicht sofort in Turnierregeln zu versinken. Gleichzeitig berührt sie alle relevanten Schichten:
FrontendAPI GatewayBackend-ServiceDatenbankAuth-KontextTenant-/VereinskontextUIi18nValidierungDamit ist sie ideal, um die Architektur einmal vollständig zu beweisen.
Was der erste Slice zeigen soll
Abschnitt betitelt „Was der erste Slice zeigen soll“Der erste Slice soll nicht nur beweisen, dass ein Formular gespeichert werden kann.
Er soll zeigen, ob die gewählte Struktur trägt.
Für die Spieler-Verwaltung bedeutet das:
User öffnet die Spieler-Seite.User sieht Spieler im aktuellen Vereinskontext.User legt einen Spieler an.Der Spieler wird serverseitig dem Verein zugeordnet.Der Spieler erhält Audit-Informationen.Die API ist geschützt.Die Datenbank speichert den Datensatz.Die UI aktualisiert sich.Das ist kein großes Feature. Aber es ist ein kompletter Weg durch das System.
Genau deshalb ist es ein guter erster fachlicher Schritt.
Der vertikale Schnitt im Frontend
Abschnitt betitelt „Der vertikale Schnitt im Frontend“Eine wichtige Entscheidung betrifft die Frontend-Struktur.
Die naheliegende technische Ordnung wäre:
libs/tournament/modellibs/tournament/domainlibs/tournament/presentationDas sieht auf den ersten Blick sauber aus. Modelle liegen bei Modellen. Domain-Code liegt bei Domain-Code. Presentation-Code liegt bei Presentation-Code.
Das Problem: Diese Struktur ist horizontal.
Mit wachsender Fachlichkeit würde dort schnell alles landen:
libs/tournament/model├─ player.model.ts├─ tournament.model.ts├─ schedule.model.ts├─ match.model.ts└─ standings.model.tslibs/tournament/domain├─ players.store.ts├─ tournaments.store.ts├─ schedule.store.ts└─ standings.store.tslibs/tournament/presentation├─ players-page.component.ts├─ tournament-setup-page.component.ts├─ schedule-page.component.ts└─ standings-page.component.tsFormal wäre das sortiert. Fachlich wäre es aber bereits vermischt.
Deshalb wird die Fachlichkeit vertikal geschnitten.
Für die Spieler-Verwaltung entsteht eine eigene Struktur:
libs/tournament/players/├─ model/├─ domain/└─ presentation/Spätere Fachlichkeiten bekommen ihre eigenen Bereiche:
libs/tournament/schedule/├─ model/├─ domain/└─ presentation/
libs/tournament/tournaments/├─ model/├─ domain/└─ presentation/Der wichtige Punkt ist:
Players bleibt bei Players. Schedule bleibt bei Schedule. Tournament bleibt bei Tournament.
Das klingt banal. Ist es aber nicht.
Gerade in Frontend-Projekten entsteht der Big Ball of Mud oft nicht dadurch, dass niemand Struktur wollte. Er entsteht dadurch, dass die Struktur zu technisch gedacht wurde.
Warum Feature-Libs?
Abschnitt betitelt „Warum Feature-Libs?“Die Entscheidung für eigene Feature-Libs ist bewusst.
Sie soll mehrere Dinge leisten:
- klare Boundaries
- besserer Fokus beim Entwickeln
- kleinere Review-Flächen
- bessere Führung für Coding-Agenten
- weniger versehentliche Kopplung
- einfachere spätere Extraktion
- weniger Druck, alles in
sharedzu verschieben
Ein späterer Auftrag kann dadurch klar begrenzt werden:
Arbeite nur in libs/tournament/players/**.Keine Änderungen an schedule.Keine Änderungen an tournaments.Das ist für menschliche Entwickler hilfreich.
Für Coding-Agenten ist es fast noch wichtiger.
Ein Agent orientiert sich stark an vorhandenen Mustern. Wenn die erste Fachlichkeit sauber vertikal liegt, ist die Wahrscheinlichkeit höher, dass spätere Fachlichkeiten ebenfalls so entstehen.
Wenn die erste Fachlichkeit dagegen in globalen Ordnern verteilt ist, wird genau diese Verteilung zum neuen Muster.
Keine Frontend-Datenweitergabe zwischen Features
Abschnitt betitelt „Keine Frontend-Datenweitergabe zwischen Features“Eine weitere Regel wird direkt mit dem ersten Slice festgelegt:
Fachliche Features kommunizieren im Frontend nicht über fremde Stores.
Konkret:
Die Spieler-Verwaltung darf Spieler laden, anzeigen, anlegen und löschen.
Aber sie wird nicht zur globalen Datenquelle für Schedule, Turnier-Setup oder andere spätere Features.
Wenn Schedule später Spieler benötigt, dann greift Schedule nicht einfach auf den PlayersStore zu.
Stattdessen holt sich Schedule seine Daten über einen eigenen Backend-Endpunkt.
Warum?
Weil verschiedene Fachlichkeiten oft unterschiedliche Sichten auf dieselben Daten brauchen.
Die Spieler-Verwaltung braucht vielleicht:
VornameNachnameinitiales RankingGeburtsdatumStatusDas Turnier-Setup braucht vielleicht:
Spieler-IDAnzeigenameRankingVerfügbarkeitbereits zugeordnetSchedule braucht vielleicht:
Spieler-IDAnzeigenameRanking-Snapshot für dieses TurnierDas sind nicht zwingend dieselben Modelle.
Wenn man zu früh ein globales PlayerModel baut, hängt später alles an allem.
Deshalb gilt für dieses Projekt:
Wiederverwendung ist nicht automatisch besser als eine klare Grenze.
Kontrollierte Doppelung ist erlaubt.
Der vertikale Schnitt im API Gateway
Abschnitt betitelt „Der vertikale Schnitt im API Gateway“Auch das API Gateway wird fachlich geschnitten.
Nicht so:
controllers/services/dto/Sondern so:
apps/apis/tournament-api/src/app/├─ players/│ ├─ dto/│ ├─ players.controller.ts│ ├─ players.service.ts│ └─ players.module.ts├─ tournaments/├─ schedule/└─ auth/Der players.service.ts im API Gateway ist dabei kein Domain-Service.
Er ist ein Gateway-Service.
Seine Aufgabe ist:
- HTTP Requests annehmen
- DTOs validieren
- den authentifizierten User auswerten
- den aktuellen Vereins-/Tenant-Kontext bestimmen oder weiterreichen
- Commands und Queries an den Backend-Service weitergeben
- Responses für das Frontend mappen
Die eigentliche Fachlichkeit liegt nicht im API Gateway.
Aber die API-Struktur bleibt trotzdem fachlich nachvollziehbar.
Ein Spieler-Endpoint liegt im Spieler-Bereich. Ein späterer Schedule-Endpoint liegt im Schedule-Bereich. Ein Turnier-Endpoint liegt im Turnier-Bereich.
Das verhindert nicht jede Kopplung. Aber es macht Kopplung sichtbarer.
Der vertikale Schnitt im Backend-Service
Abschnitt betitelt „Der vertikale Schnitt im Backend-Service“Der Backend-Service bleibt ein einzelner Service.
Es gibt also keinen eigenen players-service, keinen eigenen schedule-service und keinen eigenen tournaments-service.
Für den MVP wäre das unnötig.
Trotzdem wird auch der Service intern vertikal geschnitten:
apps/services/tournament-service/src/app/├─ players/│ ├─ dto/│ ├─ player.entity.ts│ ├─ players.repository.ts│ ├─ players.service.ts│ └─ players.module.ts├─ tournaments/├─ schedule/└─ shared/Das ist der Kern der bisherigen Architekturentscheidung:
Ein Deployment-Monolith, aber kein fachlicher Mischmasch.
Die Fachmodule teilen sich am Ende eine Datenbank. Aber sie sollen nicht beliebig ihre Controller, DTOs, Repositories und Use Cases vermischen.
Die gemeinsame Datenbank ist eine Betriebsentscheidung.
Sie ist kein Freifahrtschein für einen globalen Repository-Ordner.
Doppelung ist erlaubt
Abschnitt betitelt „Doppelung ist erlaubt“Ein wichtiger Punkt in dieser Architekturfindung ist der bewusste Umgang mit Doppelung.
Viele Teams versuchen sehr früh, gemeinsame Modelle, DTOs oder Services zu extrahieren.
Das wirkt ordentlich.
Ist aber oft gefährlich.
Denn am Anfang ist noch nicht klar, welche Ähnlichkeiten stabil sind und welche nur zufällig gleich aussehen.
Ein PlayerVm in der Spieler-Verwaltung ist nicht automatisch dasselbe wie ein PlayerSelectionItem im Turnier-Setup.
Ein PlayerDto für CRUD ist nicht automatisch dasselbe wie ein ScheduledPlayerSnapshot für einen Spielplan.
Daher gilt:
Lieber kleine, kontrollierte Doppelung als falsche Wiederverwendung.
Gemeinsame Modelle wandern erst dann nach shared, wenn die Wiederverwendung fachlich stabil ist.
Nicht, weil zwei Properties zufällig gleich heißen.
Tenant oder Verein?
Abschnitt betitelt „Tenant oder Verein?“Schon beim ersten fachlichen Feature stellt sich die Frage nach Datenräumen.
Aktuell geht es um ein internes Vereinsturnier. Aber die Modellierung sollte nicht so eng sein, dass spätere Erweiterungen unnötig schwer werden.
Was passiert, wenn später mehrere Vereine die App nutzen?
Was passiert, wenn ein Admin einen Verein anlegt und Trainer dazu einlädt?
Was passiert, wenn später Turniere über mehrere Vereine hinweg stattfinden?
Das soll jetzt nicht gebaut werden.
Aber es soll auch nicht verbaut werden.
Die saubere fachliche Sicht ist:
Der Verein ist der Tenant.
Technisch könnte man also von tenantId sprechen. Fachlich ist clubId vermutlich der bessere Begriff.
Ein späteres Zielbild könnte so aussehen:
Club├─ Admins├─ Trainers├─ Players└─ TournamentsMit Mitgliedschaften:
ClubMembership├─ clubId├─ userId├─ role└─ statusFür den MVP wird das noch nicht implementiert.
Aber die Player-Daten sollen bereits einem Vereinskontext zugeordnet werden.
sub, clubId und Audit
Abschnitt betitelt „sub, clubId und Audit“Durch die Keycloak-Integration gibt es bereits eine stabile technische User-ID:
userId = token.subDiese User-ID kommt aus dem Access Token und wird vom API Gateway validiert.
Für die Datenmodellierung bedeutet das:
createdByUserIdupdatedByUserIdDiese Felder sind Audit-Informationen.
Sie beantworten:
Wer hat den Datensatz angelegt oder zuletzt verändert?
Der fachliche Datenraum ist davon getrennt.
Dieser wird durch den Verein bestimmt:
clubIdDamit ergibt sich eine wichtige Regel:
subidentifiziert den Benutzer.clubIdidentifiziert den fachlichen Datenraum.createdByUserIdist Audit, nicht automatisch die Tenant-Grenze.
Für den ersten Durchstich kann clubId noch aus einer Konfiguration kommen, zum Beispiel aus einer Default-Club-ID.
Das Frontend setzt diese ID nicht selbst.
Der Server bestimmt den Kontext.
Warum nicht nach createdByUserId filtern?
Abschnitt betitelt „Warum nicht nach createdByUserId filtern?“Eine naheliegende einfache Absicherung wäre:
where: { clubId: currentClubId, createdByUserId: currentUser.userId}Das ist sehr restriktiv.
Aber für eine Vereins-App ist es fachlich wahrscheinlich falsch.
Wenn ein Admin Spieler anlegt, sollen Trainer diese Spieler später vermutlich auch sehen und für Turniere verwenden können. Die Spieler gehören nicht dem einzelnen Benutzer. Sie gehören dem Verein.
Deshalb ist die bessere Richtung:
where: { clubId: currentClubId;}Die Prüfung, ob ein Nutzer diesen clubId-Kontext überhaupt verwenden darf, erfolgt später über Memberships und Rollen.
Für den MVP kann diese Membership-Logik noch fehlen. Aber die Modellierung sollte bereits in die richtige Richtung zeigen.
Der erste fachliche Durchstich: Players CRUD
Abschnitt betitelt „Der erste fachliche Durchstich: Players CRUD“Der erste fachliche Slice ist die Spieler-Verwaltung.
Bewusst klein:
ListeAnlegenLöschen beziehungsweise DeaktivierenKeine Detailseite.
Keine komplexe Bearbeitung.
Keine Turnierzuordnung.
Kein Schedule.
Kein Drag & Drop.
Die UI kann zunächst aus einer Übersichtsseite bestehen:
- Tabelle mit Spielern
- Button “Spieler anlegen”
- Modal/Dialog für Create
- Aktion pro Zeile zum Löschen oder Deaktivieren
Wichtig ist nicht, dass dieses Feature spektakulär ist.
Wichtig ist, dass es den ersten fachlichen Durchstich sauber beweist.
Löschen oder Deaktivieren?
Abschnitt betitelt „Löschen oder Deaktivieren?“Bei Spielern sollte man vorsichtig mit echtem Löschen sein.
Für ein MVP ist ein Delete-Button in der UI verständlich. Fachlich kann dahinter aber ein Soft Delete beziehungsweise eine Deaktivierung stehen.
Also:
UI: LöschenBackend: DeaktivierenDas hat Vorteile:
- versehentlich gelöschte Spieler sind nicht sofort aus der Historie verschwunden
- spätere Turniere können auf alte Spieler referenzieren
- Audit bleibt möglich
- das Modell ist robuster für Historie
Für den ersten Durchstich reicht eine einfache Entscheidung:
Kein Hard Delete, wenn Historie später relevant werden kann.Auch hier gilt: nicht überbauen, aber nicht verbauen.
Was bewusst nicht gebaut wird
Abschnitt betitelt „Was bewusst nicht gebaut wird“Der erste fachliche Slice bleibt eng.
Nicht enthalten:
TurnierlisteTurnier-SetupGruppeneinteilungDrag & DropSpielplan-GeneratorErgebniserfassungEltern-Live-AnsichtVereinsverwaltungEinladungenRollenmatrixMembership-VerwaltungDas mag hart wirken. Aber genau diese Begrenzung schützt das Projekt.
Ein großer fachlicher Prompt würde sehr schnell wieder mehrere Grenzen gleichzeitig berühren. Dann wäre schwer zu erkennen, ob ein Problem aus der UI, der API, dem Service, der Datenbank, der Auth oder der Modellierung kommt.
Der erste Slice soll klein bleiben, damit sein Review möglich bleibt.
Was dieser Schritt für spätere Features vorbereitet
Abschnitt betitelt „Was dieser Schritt für spätere Features vorbereitet“Wenn der Players-Slice sauber sitzt, ist viel gewonnen.
Dann gibt es ein Muster für:
- vertikale FE-Libs
- API Gateway Fachordner
- Service Fachmodule
- geschützte Endpoints
- Auth-Kontext im Request
clubIdals Datenraum- Audit über
createdByUserId - i18n in der UI
- PrimeNG-Komponenten
- Tailwind-Layout
- kleine fachliche Prompts
Spätere Features können sich daran orientieren.
Zum Beispiel Turnier-Setup:
libs/tournament/tournaments/modellibs/tournament/tournaments/domainlibs/tournament/tournaments/presentationOder Schedule:
libs/tournament/schedule/modellibs/tournament/schedule/domainlibs/tournament/schedule/presentationAber sie greifen nicht einfach in Players hinein.
Sie bekommen eigene Backend-Abfragen und eigene ViewModels.
Das ist der Schutz vor dem Big Ball of Mud.
Der erste fachliche Durchstich ist bewusst unspektakulär.
Spieler anzeigen. Spieler anlegen. Spieler löschen oder deaktivieren.
Aber architektonisch ist dieser Schritt wichtig.
Er entscheidet, ob die Anwendung fachlich vertikal wächst oder ob sie direkt wieder in technische Sammelordner zerfasert.
Die zentrale Entscheidung lautet:
Nicht erst alles global sammeln und später hoffen, dass man es wieder sortiert bekommt. Sondern von Anfang an fachlich schneiden.
Für dieses Projekt bedeutet das:
Frontend vertikal nach FachlichkeitAPI Gateway vertikal nach FachlichkeitBackend-Service vertikal nach Fachlichkeitgemeinsame Datenbankbewusste Doppelungkleines SharedclubId als Datenraumsub als BenutzeridentitätDas ist kein großer Enterprise-Blueprint.
Es ist eine kleine, pragmatische Strukturentscheidung, damit aus dem MVP kein Big Ball of Mud wird.
Und genau deshalb beginnt die Fachlichkeit mit Players.