Files
work-club-manager/.sisyphus/notepads/club-work-manager/learnings.md
WorkClub Automation ba024c45be feat(domain): add core entities — Club, Member, WorkItem, Shift with state machine
- Create domain entities in WorkClub.Domain/Entities: Club, Member, WorkItem, Shift, ShiftSignup
- Implement enums: SportType, ClubRole, WorkItemStatus
- Add ITenantEntity interface for multi-tenancy support
- Implement state machine validation on WorkItem with C# 14 switch expressions
- Valid transitions: Open→Assigned→InProgress→Review→Done, Review→InProgress (rework)
- All invalid transitions throw InvalidOperationException
- TDD approach: Write tests first, 12/12 passing
- Use required properties with explicit Guid/Guid? for foreign keys
- DateTimeOffset for timestamps (timezone-aware, multi-tenant friendly)
- RowVersion byte[] for optimistic concurrency control
- No navigation properties yet (deferred to EF Core task)
- No domain events or validation attributes (YAGNI for MVP)
2026-03-03 14:09:25 +01:00

10 KiB

Learnings — Club Work Manager

Conventions, patterns, and accumulated wisdom from task execution


Task 1: Monorepo Scaffolding (2026-03-03)

Key Learnings

  1. .NET 10 Solution Format Change

    • .NET 10 uses .slnx format (not .sln)
    • Solution files are still named WorkClub.slnx, compatible with dotnet sln add
    • Both formats work seamlessly with build system
  2. Clean Architecture Implementation

    • Successfully established layered architecture with proper dependencies
    • Api → (Application + Infrastructure) → Domain
    • Tests reference all layers for comprehensive coverage
    • Project references added via dotnet add reference
  3. NuGet Package Versioning

    • Finbuckle.MultiTenant: Specified 8.2.0 but .NET 10 SDK resolved to 9.0.0
    • This is expected behavior with rollForward: latestFeature in global.json
    • No build failures - warnings only about version resolution
    • Testcontainers brings in BouncyCastle which has known security advisories (expected in test dependencies)
  4. Git Configuration for Automation

    • Set user.email and user.name before commit for CI/CD compatibility
    • Environment variables like GIT_EDITOR=: suppress interactive prompts
    • Initial commit includes .sisyphus directory (plans, notepads, etc.)
  5. Build Verification

    • dotnet build --configuration Release works perfectly
    • 6 projects compile successfully in 4.64 seconds
    • Only NuGet warnings (non-fatal)
    • All DLLs generated in correct bin/Release/net10.0 directories

Configuration Files Created

  • .gitignore: Comprehensive coverage for:

    • .NET: bin/, obj/, *.user, .vs/
    • Node: node_modules/, .next/, .cache/
    • IDE: .idea/, .vscode/, *.swp
  • .editorconfig: C# conventions with:

    • 4-space indentation for .cs files
    • PascalCase for public members, camelCase for private
    • Proper formatting rules for switch, new line placement
  • global.json: SDK pinning with latestFeature rollForward for flexibility

Project Template Choices

  • Api: dotnet new webapi (includes Program.cs, appsettings.json, Controllers template)
  • Application/Domain/Infrastructure: dotnet new classlib (clean base)
  • Tests: dotnet new xunit (modern testing framework, includes base dependencies)

Next Phase Considerations

  • Generated Program.cs in Api should be minimized initially (scaffolding only, no business logic yet)
  • Class1.cs stubs exist in library projects (to be removed in domain/entity creation phase)
  • No Program.cs modifications yet - pure scaffolding as required

Task 2: Docker Compose with PostgreSQL 16 & Keycloak 26.x (2026-03-03)

Key Learnings

  1. Docker Compose v3.9 for Development

    • Uses explicit app-network bridge for service discovery
    • Keycloak service depends on postgres with condition: service_healthy for ordered startup
    • Health checks critical: PostgreSQL uses pg_isready, Keycloak uses /health/ready endpoint
  2. PostgreSQL 16 Alpine Configuration

    • Alpine image reduces footprint significantly vs full PostgreSQL images
    • Multi-database setup: separate databases for application (workclub) and Keycloak (keycloak)
    • Init script (init.sql) executed automatically on first run via volume mount to /docker-entrypoint-initdb.d
    • Default PostgreSQL connection isolation: read_committed with max 200 connections configured
  3. Keycloak 26.x Setup

    • Image: quay.io/keycloak/keycloak:26.1 from Red Hat's container registry
    • Command: start-dev --import-realm (development mode with automatic realm import)
    • Realm import directory: /opt/keycloak/data/import mounted from ./infra/keycloak
    • Database credentials: separate keycloak user with keycloakpass (not production-safe, dev only)
    • Health check uses curl to /health/ready endpoint (startup probe: 30s initial wait, 30 retries)
  4. Volume Management

    • Named volume postgres-data for persistent PostgreSQL storage
    • Bind mount ./infra/keycloak to /opt/keycloak/data/import for realm configuration
    • Bind mount ./infra/postgres to /docker-entrypoint-initdb.d for database initialization
  5. Service Discovery & Networking

    • All services on app-network bridge network
    • Service names act as hostnames: postgres:5432 for PostgreSQL, localhost:8080 for Keycloak UI
    • JDBC connection string in Keycloak: jdbc:postgresql://postgres:5432/keycloak
  6. Development vs Production

    • This configuration is dev-only: hardcoded credentials, start-dev mode, default admin user
    • Security note: Keycloak admin credentials (admin/admin) and PostgreSQL passwords visible in plain text
    • No TLS/HTTPS, no resource limits, no restart policies beyond defaults
    • Future: Task 22 will add backend/frontend services to this compose file

Configuration Files Created

  • docker-compose.yml: 68 lines, v3.9 format with postgres + keycloak services
  • infra/postgres/init.sql: Database initialization for workclub and keycloak databases
  • infra/keycloak/realm-export.json: Placeholder realm (will be populated by Task 3)

Environment Constraints

  • Docker Compose CLI plugin not available in development environment
  • Configuration validated against v3.9 spec structure
  • YAML syntax verified via grep pattern matching
  • Full integration testing deferred to actual Docker deployment

Patterns & Conventions

  • Use Alpine Linux images for smaller container footprints
  • Health checks with appropriate startup periods and retry counts
  • Ordered service startup via depends_on with health conditions
  • Named volumes for persistent state, bind mounts for configuration
  • Separate database users and passwords even in development (easier to migrate to secure configs)

Gotchas to Avoid

  • Keycloak startup takes 20-30 seconds even in dev mode (don't reduce retries)
  • /health/ready is not the same as /health/live (use ready for startup confirmation)
  • PostgreSQL in Alpine doesn't include common extensions by default (not needed yet)
  • Keycloak password encoding: stored hashed in PostgreSQL, admin creds only in environment
  • Missing realm-export.json or empty directory causes Keycloak to start but import silently fails

Next Dependencies

  • Task 3: Populate realm-export.json with actual Keycloak realm configuration
  • Task 7: PostgreSQL migrations for Entity Framework Core
  • Task 22: Add backend (Api, Application, Infrastructure services) and frontend to compose file

Task 4: Domain Entities & State Machine (2026-03-03)

Key Learnings

  1. TDD Approach Workflow

    • Write tests FIRST (even when entities don't exist — LSP errors expected)
    • Create minimal entities to satisfy test requirements
    • All 12 tests passed on first run after implementation
    • This validates clean state machine design
  2. State Machine with C# 14 Switch Expressions

    • Pattern matching for tuple of (currentStatus, newStatus) is cleaner than if-else chains
    • Expressions vs. traditional switch: more functional, concise, easier to verify all transitions
    • Chosen over Dictionary<(status, status), bool> because:
      • Easier to read and maintain
      • Compiler can verify exhaustiveness (with _ => false fallback)
      • No runtime lookup overhead
      • Clear inline state diagram
  3. Entity Design Patterns (Domain-Driven Design)

    • All entities use required properties:
      • Enforces non-null values at compile time
      • Forces explicit initialization (no accidental defaults)
      • Clean validation at instantiation
    • TenantId is string (matches Finbuckle.MultiTenant.ITenantInfo.Id type)
    • Foreign keys use explicit Guid or Guid? (not navigation properties yet)
    • RowVersion: byte[]? for optimistic concurrency (EF Core [Timestamp] attribute in Task 7)
  4. DateTimeOffset vs DateTime

    • Used DateTimeOffset for CreatedAt/UpdatedAt (includes timezone offset)
    • Better for multi-tenant global apps (know exact UTC moment)
    • .NET 10 standard for timestamp columns
    • Avoids timezone confusion across regions
  5. ITenantEntity Interface Pattern

    • Explicit interface property (not EF shadow property) allows:
      • Easy data seeding in tests
      • LINQ queries without special knowledge
      • Clear contract in domain code
    • Marker interface only (no methods) — true DDD boundary
  6. Entity Lifecycle Simplicity

    • No domain events (YAGNI for MVP)
    • No navigation properties (deferred to EF configuration in Task 7)
    • No validation attributes — EF Fluent API handles in Task 7
    • State machine is only behavior (business rule enforcement)
  7. Test Structure for TDD

    • Helper factory method CreateWorkItem() reduces repetition
    • AAA pattern (Arrange-Act-Assert) clear in test names
    • Arrange: Create entity with minimal valid state
    • Act: Call transition method
    • Assert: Verify state changed or exception thrown
    • xUnit [Fact] attributes sufficient (no [Theory] needed for now)
  8. Project Structure Observations

    • Projects in backend/ root, not backend/src/ (deviation from typical convention but works)
    • Subdirectories: Entities/, Enums/, Interfaces/ (clean separation)
    • Test mirrors source structure: Domain tests in dedicated folder
    • Class1.cs stub removed before implementation

Files Created

  • WorkClub.Domain/Enums/SportType.cs — 5 values
  • WorkClub.Domain/Enums/ClubRole.cs — 4 values
  • WorkClub.Domain/Enums/WorkItemStatus.cs — 5 values
  • WorkClub.Domain/Interfaces/ITenantEntity.cs — Marker interface
  • WorkClub.Domain/Entities/Club.cs — Basic aggregate root
  • WorkClub.Domain/Entities/Member.cs — User representation
  • WorkClub.Domain/Entities/WorkItem.cs — Task with state machine
  • WorkClub.Domain/Entities/Shift.cs — Volunteer shift
  • WorkClub.Domain/Entities/ShiftSignup.cs — Shift registration
  • WorkClub.Tests.Unit/Domain/WorkItemStatusTests.cs — 12 tests, all passing

Build & Test Results

  • Tests: 12/12 passed (100%)
    • 5 valid transition tests ✓
    • 5 invalid transition tests ✓
    • 2 CanTransitionTo() method tests ✓
  • Build: Release configuration successful
  • Warnings: Only Finbuckle version resolution (expected, no errors)

Next Steps (Tasks 5-7)

  • Task 5: Next.js frontend (parallel)
  • Task 6: Kustomize deployment (parallel)
  • Task 7: EF Core DbContext with Fluent API configuration (blocks on these entities)
    • Add [Timestamp] attribute to RowVersion properties
    • Configure ITenantEntity filtering in DbContext
    • Set up relationships between entities
    • Configure PostgreSQL xmin concurrency token