Model the whole domain: communities, sessions, ratings and operations - #33
Merged
Conversation
The schema was a thin slice — players, teams, matches, tournaments — with an EloRating class that nothing outside the tests ever called. It carried no community, no scheduling, no money, and no notion of format, so 2v2 and 6v6 were indistinguishable. This lays down the model the product actually needs, across eleven bounded contexts, in five migrations that each apply and revert on their own. Three changes are structural and worth calling out: - Rating moves from Player to CommunityMember. A player's standing is relative to the people they play with, not global. - MatchAppearance records who actually played, with the rating before, after and delta. Teams are lineups and change; who played a given match does not. This is what makes ratings explainable and replayable. - Matches snapshot the scoring they were played under, so amending a rule set cannot rewrite the past. Elo is no longer dead code: MatchRatingCalculator sits on top of it and MatchQueries.CreateAsync now applies ratings on entry. Community and format are derived from the teams — a team already belongs to a community, and its size is the format — so the client needed no new fields. Money is double-entry from the start, in minor units, which makes dues, session fees, expenses and sponsorship one mechanism rather than four. Constraints the database enforces rather than the application: court reservations cannot overlap (EXCLUDE USING gist), ledger amounts are positive and move between two different accounts, an item is on loan to one person at a time, and a community has at most one current season. Each migration was applied and reverted against Postgres 18, including the backfills: player slugs are derived and de-duplicated, ratings survive the move in both directions, and appearances are reconstructed from existing lineups. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lays down the data model the product needs, replacing a schema that was players,
teams, matches and tournaments — with an
EloRatingclass nothing outside thetests ever called.
Refs #32.
What changes
45 entities across eleven bounded contexts, one EF configuration per entity, and
five migrations that each apply and revert on their own.
Three changes are structural:
PlayertoCommunityMember. A player’s standing isrelative to the people they play with, not global.
MatchAppearancerecords who actually played, with the rating before,after and delta. Teams are lineups and change; who played a given match does
not. This is what makes ratings explainable and replayable.
set cannot rewrite the past.
Elo is no longer dead code.
MatchRatingCalculatorsits on top ofEloRating, andMatchQueries.CreateAsyncapplies ratings on entry. Communityand format are derived from the teams — a team already belongs to a community,
and its size is the format — so the WebAssembly client needed no new fields.
Money is double-entry in minor units, which makes dues, session fees, expenses
and sponsorship one mechanism rather than four.
Enforced by the database
Application-level checks race, so these are constraints:
EXCLUDE USING gist, half-open ranges)Verification
Each migration was applied against a throwaway Postgres 18, then the whole chain
reverted (46 tables back to 7) and rolled forward again.
jonas-mueller,jonas-mueller-2),German transliterated (
strassburger-weiss), degenerate names fall back toplayerDown()carries them backbefore the memberships holding them are dropped
cancelled ignored, inverted range rejected
without a member all rejected
flat because the deltas that produced them were never recorded
22 tests pass. Release build clean,
dotnet format --verify-no-changesclean.Worth knowing when reviewing
The
Locationbackfill is a no-op in practice. The intent was to matchfree text against courts, but no courts exist at that point in the chain, so
it all lands in
LocationNote. EF detected a column rename, so nothing waslost. Turning those strings into venues would have meant fabricating data.
TeamMemberkept its composite key. A surrogateIdwas planned;nothing needs one, and adding it means a primary key rebuild for no gain.
PositionandSortOrderwere added as intended.The rating write path has no integration test. The calculator is well
covered and the schema is verified, but the glue in
CreateAsyncis not —there is no infrastructure test project. See Integration test for the rating write path #34.
Entities are grouped into folders by context but keep the flat
Ssabba.Domain.Entitiesnamespace, so no existingusingchurned.IDE0130is silenced in.editorconfigwith a note.🤖 Generated with Claude Code