From CRUD to a planning flow
The tournament app’s first domain vertical slice was deliberately small: manage players.
That was the right entry point. A CRUD feature is well suited to proving out the architecture end to end: frontend, API gateway, service, database, auth context, and club/tenant context.
With tournament planning, the next step arrived.
This time it wasn’t just one form and one list anymore. It was a multi-step domain flow:
Create tournamentSelect playersForm groupsGenerate scheduleComplete planningThat’s what turned the project into more than a CRUD example for the first time.
The players slice had shown that the architecture holds up in principle.
The tournament setup flow showed where it gets harder: derived state, wizard state, routing context, persistence boundaries, and reactivity across multiple steps.
The domain boundary
Section titled “The domain boundary”Tournament planning was deliberately not built into player management.
Players are their own domain area. Tournament setup is a different one.
So a new vertical area got created:
libs/tournament/tournament-setup/modellibs/tournament/tournament-setup/domainlibs/tournament/tournament-setup/presentationThe API gateway and service got organized around the same domain boundary too:
apps/apis/tournament-api/src/app/tournament-setupapps/services/tournament-service/src/app/tournament-setupThe goal was clear:
Setup is allowed to use players, but must not become dependent on the players frontend.
Tournament planning loads its own view of active players. It doesn’t reach into a PlayersStore. That’s deliberately a bit more effort, but it protects the domain boundary.
A player in player management isn’t automatically the same ViewModel as a selectable tournament participant.
The tournament planning flow
Section titled “The tournament planning flow”The use case currently consists of three steps:
1. Select players2. Form groups3. Generate scheduleThe entry point sits on the tournament setup overview.
From there, you can either start a new tournament or continue working on an existing open one. As soon as a specific tournament is open, you’re inside the tournament context.
This separation mattered more than it seemed to at first.
/tournament-setup means:
OverviewA specific tournament means:
/tournament-setup/:tournamentId/players/tournament-setup/:tournamentId/groups/tournament-setup/:tournamentId/scheduleOnly through this separation did it become clear when the app should show a list of existing tournaments and when it’s inside an active planning process.
Selecting players
Section titled “Selecting players”In the first step, active players get displayed and selected.
Player selection is deliberately not access to the existing player management. Setup gets its own API view:
GET /tournament-setup/available-playersThe UI shows the players in a grid. Selected players are clearly marked.
The validation is deliberately simple:
at least 4 playersan even number of playersIt’s not decided yet, at this point, who actually plays and who stays a reserve. That happens during group formation.
Forming groups
Section titled “Forming groups”In the second step, the selected players get assigned to groups.
Group formation allows for a player pool. Not every selected player has to be assigned to a group. Players who stay in the pool get persisted as reserves later.
For the MVP, the plannable group sizes were deliberately limited:
4 players6 players8 players10 playersThat’s an important simplification.
Instead of building a general rule set for every conceivable group size right away, only what the current tournament format actually needs gets supported.
Players may remain in the pool. But groups themselves need to have a supported size.
Valid: 4, 6, 8, or 10 players per groupInvalid: 2, 5, 12, or other sizesGenerating the schedule
Section titled “Generating the schedule”The third step generates the schedule for the first group phase.
As soon as the schedule tab gets opened, the backend checks:
Does a schedule already exist?If yes, it gets loaded.
If not, and the tournament has the GROUPS_READY status, the schedule gets generated and persisted. After that, the status gets set to SCHEDULE_READY.
That completes the planning.
Important: in the MVP, there’s no redraw and no manual editing of the schedule.
That’s intentional.
A schedule shouldn’t get redrawn arbitrarily until someone likes it better. Editability or new draws can become their own use case later. For the MVP, the schedule stays stable.
Why round 2 isn’t planned yet
Section titled “Why round 2 isn’t planned yet”Round 2 sounds similar at first glance.
Groups again. Doubles again. Pairings again.
From a domain perspective, however, it is different.
In round 1, the players are known. They come from group formation.
In round 2, the players only emerge from results.
Top 4 from group ATop 4 from group Bor a comparable qualification model.
That means round 2 can’t be cleanly planned before results and standings exist.
That’s why tournament planning currently ends at:
SCHEDULE_READYTournament day later begins with:
ACTIVEThat’s where result recording, standings, qualification, and round 2 belong.
Rule set for the first group phase
For the MVP, the schedule generator supports four group sizes:
4 players6 players8 players10 playersThe planned number of games per player:
4 players → 1 game per player6 players → 2 games per player8 players → 3 games per player10 players → 4 games per playerSince it’s always doubles, a match consists of four players:
2 players against 2 playersThat gives the following match counts:
4 players:1 round1 matchno bye
6 players:3 rounds1 match per round2 players get a bye per round
8 players:3 rounds2 matches per roundno bye
10 players:5 rounds2 matches per round2 players get a bye per roundFor a group of 10, that means:
10 players × 4 games per player = 40 player appearances1 doubles match = 4 player appearances40 / 4 = 10 matchesWhich gives:
5 rounds × 2 matches per round = 10 matchesQuality goals:
The number of games per player checks out.Byes are distributed fairly.Partner pairings repeat as rarely as possible.Opponent pairings repeat as rarely as possible.Reserve players don't get scheduled.In the MVP, the generator works with fixed, testable templates for the supported group sizes. Later, this could turn into a configurable rule set or a real generator.
Design discovery: a screenshot isn’t enough
Section titled “Design discovery: a screenshot isn’t enough”The visual part turned out to be surprisingly involved.
At the start was a design image for inspiration: a dark interface, club colors, black and green jerseys, an athletic admin UI.
For a human, the direction was immediately clear.
Not for the coding agent.
It could roughly tell from the image that it should be dark, green, and card-based. But it repeatedly missed the actual structure.
Typical problems:
cards inside cardsrestless meta areaswrong hover effectsjumping borderswhite default surfaces in a dark UIimprecise spacinginconsistent arrangementThe takeaway was clear:
For the agent, a screenshot is a moodboard, not a layout contract.
A classic grid system worked better.
Why the 12-column grid helped
Section titled “Why the 12-column grid helped”Instead of “make it look like the screenshot,” prompts worked better when they described the layout explicitly.
For example:
12-column grid
Desktop:- Group area: col-span-9- Side panel: col-span-3
Tablet/Mobile:- Group area: col-span-12- Side panel: col-span-12Or for player cards:
Desktop:- 4 cards per row- each card col-span-3
Tablet:- 2 cards per row- each card col-span-6
Mobile:- 1 card per row- each card col-span-12That was far more tangible than a purely visual target.
Flexbox is powerful, but often too open-ended in prompts. An agent then interprets a lot on its own. A grid with columns, areas, and breakpoints is more verifiable in comparison.
So the new rule for layout prompts is:
Images provide mood. The 12-column grid provides the contract.
Small layout contracts instead of big wishes
Section titled “Small layout contracts instead of big wishes”A good layout prompt doesn’t just describe how something should feel — it describes where it sits.
Bad:
Make the list more professional.Better:
Player row uses a 12-column grid.Avatar on the left.Name and ranking in the main area.Status on the right.Actions all the way to the right.No nested cards.No hover effects on non-clickable rows.This kind of instruction is less poetic, but much more effective.
That matters especially with AI agents. They don’t just need direction — they need hard boundaries.
Drift: when familiar patterns work against modern architecture
Section titled “Drift: when familiar patterns work against modern architecture”The next big lesson was architecture drift.
The app uses modern Angular approaches with signals, stores, derived state, and increasingly resource-based data flows too.
But the agent repeatedly fell back into older patterns.
That was especially visible when loading data.
Instead of using a reactive data flow, it repeatedly wanted to build a classic initial load:
Component starts→ ngOnInit→ load()→ write data into local stateThat’s a pattern you find in a great many Angular codebases. It’s not fundamentally wrong. But it doesn’t always fit modern signal-based architecture.
My hypothesis: a large share of the training material and community examples still relies on these older imperative set-and-patch patterns.
So:
loadsetpatchsync manuallyModern Angular APIs like resource or httpResource shift the focus. Data gets modeled more strongly as a reactive dependency. An httpResource creates a resource for HTTP GET data and updates itself when the signal-based request description changes. It still uses Angular’s HttpClient and its infrastructure underneath, including interceptors.
That means: not every data flow needs an explicit initial load in the old style.
A quick look at resources
Section titled “A quick look at resources”You can think of a resource as an asynchronous dependency whose value gets exposed through signals.
httpResource is the HTTP-adjacent variant of that. It’s meant for GET-based data flows and updates itself when its input changes via signals.
That fits well with flows like:
activeTournamentId changes→ resource loads the matching tournamentOr:
Route param changes→ schedule resource loads the current scheduleThe parse option is interesting too. It lets you transform raw HTTP data, or validate it with a runtime schema library like Zod, before it gets delivered to the resource.
That fits well with our architecture idea:
DTO→ parse / zod / ACL→ ViewModel→ ComponentBut this particular kind of data flow hasn’t fully landed in the agent’s responses everywhere yet.
Why this led to bugs
Section titled “Why this led to bugs”Several bugs came from unclear state ownership.
For example:
Tournament got loaded, but not displayed.Player selection turned up empty.Changing a group created a new tournament.Schedule didn't get re-derived.An old schedule stayed valid even though groups had changed.These weren’t just typos.
They were modeling mistakes.
The core question was always:
What's the source of truth?What's derived?When does something get persisted?When does something get invalidated?This mattered especially with the schedule.
The schedule isn’t independent domain state. It’s derived from the current group setup.
If groups change, the old schedule is invalid.
That has to be clearly represented in both the backend and the frontend:
Change groups→ invalidate old schedule→ status back to GROUPS_READY→ open schedule tab→ generate new schedule→ status SCHEDULE_READYWhen the agent failed to model this relationship clearly, it produced duplicate tournaments, stale schedules, or incorrect pairings.
The most important architectural point: take derivations seriously
Section titled “The most important architectural point: take derivations seriously”The entire tournament setup flow consists of derivations.
active players→ selectable players
selected players→ group setup
group setup→ schedule
schedule→ SCHEDULE_READYIf an earlier state changes, later derivations have to become invalid.
That sounds trivial, but it was the hardest part.
Because a classic “set pattern” happily stores every intermediate state as a finished value, and then forgets to invalidate it on change.
The better mental model is:
Persisted state is the source. ViewModels and plans are derived. When the source changes, derived data has to be recreated or deliberately invalidated.
What the agent did well
Section titled “What the agent did well”Despite all the corrections, the progress was impressive.
The app today has a complete planning flow:
Create tournamentSelect playersForm groupsAccount for reservesGenerate the schedule for round 1Move the tournament status to SCHEDULE_READYThe tests got stronger too. Tests were especially important for:
Group sizesGames per playerMatch countsSchedule invalidationTenant/club contextBackend validationThat’s exactly the point where AI-assisted development works: the agent produces structure, code, and tests quickly. But the human has to recognize and defend the domain invariants.
What the agent struggled with
Section titled “What the agent struggled with”The hard parts were mostly things that can’t be solved locally, in one file.
For example:
Route conceptActive tournament contextCreate vs. updateSchedule as a derivationInvalidationState across tabsTheming across CDK/PrimeNGThese are cross-cutting concerns.
An agent can solve them, but it needs very clear prompts. As soon as the prompt is too open, it reaches for familiar patterns. And those patterns don’t always fit the architecture.
Conclusion
Section titled “Conclusion”The step from CRUD to a real planning flow has been the most interesting part of the journey so far.
Tournament planning isn’t a big product. But it already contains plenty of real architecture questions:
How do you draw feature boundaries?How do you prevent coupling?How do you model wizard state?When do you persist?When do you invalidate derivations?How do you keep UI layout agent-friendly?How do you bring modern Angular reactivity into a codebase when a lot of examples still show old patterns?The most important design takeaway:
For AI agents, a classic 12-column grid is often better than a freely described flex layout.
The most important architecture takeaway:
Agents can generate code quickly, but they need clear source-of-truth rules.
And the most important domain takeaway:
Planning round 1 is a setup problem. Round 2 is a results problem.
That’s why tournament planning deliberately stops at SCHEDULE_READY for now.
The next big use case isn’t planning anymore.
The next big use case is tournament day.