# 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