Files
event-demo/CLAUDE.md
T
flecomte 505cfe38f0
Tests / build (push) Successful in 7m19s
Tests / test (push) Failing after 10m44s
Tests / lint (push) Successful in 13m49s
chore: add CLAUDE.md
2026-08-06 19:52:48 +02:00

7.0 KiB

CLAUDE.md — event-demo

Ce fichier donne le contexte du projet pour toute session Claude Code future sur ce dépôt.

Vue d'ensemble

event-demo est un projet démo personnel (Fabrice Lecomte) qui illustre plusieurs patterns d'architecture backend :

  • Event Sourcing
  • Event-Driven (bus d'événements asynchrone)
  • CQRS (séparation commandes / projections en lecture)
  • Architecture Hexagonale (ports & adapters), un dossier par bounded context

Le cas d'usage servant de support est un jeu de cartes façon UNO (créer une partie, rejoindre, jouer une carte, piocher, etc.), avec authentification des joueurs.

Dépôts distants configurés : gitea (auto-hébergé, git.gogn.synology.me — remote historique) et github (flecomte/event-demo, miroir). Vérifier vers lequel pousser selon le contexte.

Stack technique

  • Langage : Kotlin 2.1.21, JDK 21 (toolchain Gradle)
  • Framework serveur : Ktor 3.5.1 (Netty), DI via Koin 4.2.1
  • Sérialisation : kotlinx.serialization (JSON)
  • Persistance :
    • PostgreSQL (event store, via HikariCP) + migrations Flyway (migrations/events/)
    • RabbitMQ (bus d'événements / bus de commandes, via amqp-client)
  • Auth : JWT (ktor-server-auth-jwt), hash de mot de passe via password4j
  • Infra dev/prod : Docker Compose (fichiers docker/docker-compose-{dev,test,prod}.yaml incluant des « parts » réutilisables dans docker/parts/), reverse proxy Træfik
  • Tests : Kotest (runner JUnit5), MockK, kotest-extensions-koin, ArchUnit (test d'architecture)
  • Qualité : ktlint (ktlint_official, standard + experimental activés), reporting checkstyle
  • CI : GitHub Actions (.github/workflows/tests.yml) — build/cache Gradle, ktlintCheck, puis tests exécutés dans Docker (docker compose -f docker/docker-compose-test.yaml run tests)
  • API : documentée en OpenAPI (resources/openapi/documentation.yaml)

Architecture

Un dossier par bounded context sous src/main/kotlin/eventDemo/contexts/<context>/, chacun strictement découpé en 3 couches :

  • domain/ — aucune dépendance vers les autres couches
  • application/ — ne dépend que de domain
  • infrastructure/ — dépend de domain et application

Contexts actuels :

  • auth : User, création de compte, login JWT, event store dédié (Postgresql), projection utilisateur.
  • game : cœur du jeu — Card, DrawPile/DiscardPile, Player, GameId, commandes (JoinTheGameCommand, PlayCardCommand, ReadyToPlayCommand, TakeCartFromDrawPileCommand), state machine du jeu via sealed interface Game (GameInitGameCreatedGameStartedGameEnded), notifications, projections (liste de parties), listeners/réactions.

Libs transverses dans libs/ (indépendantes de tout contexte) :

  • bus/ — abstraction Bus<E> avec implémentations in-memory et RabbitMQ (fanout exchange)
  • command/Command, CommandUnicityChecker (empêche la double exécution d'une commande, cache glissant de 10 min par défaut)
  • eventSource/Event, EventStream (append/lecture par version, gestion de VersionConflictException), EventStore in-memory / Postgresql
  • helpers/, serializer/ — utilitaires (conversion de frames WebSocket, sérialiseurs UUID, etc.)

Patterns notables dans le code

  • Event sourcing : Game.loadFromHistory(events) reconstruit l'état en repliant (fold) les événements sur une state machine scellée, en utilisant la syntaxe Kotlin 2.1 when avec garde if (ex. is GameCreatedEvent if this is GameInit -> applyEvent(event)).
  • CQRS : écriture via les command handlers (application/command/handlers), lecture via des projections dédiées (application/projections), propagées via le bus RabbitMQ, pas de couplage direct avec l'écriture.
  • Event-driven : réactions asynchrones (ReactionListener, EventToNotificationSubscriber) déclenchées par le bus RabbitMQ (exchange fanout, une queue par abonné).
  • Exceptions métier : hiérarchie GameException / IllegalActionException dans domain/game/errors, une exception par règle métier violée (ex. NeedMorePlayersToStartGameException, ItsNotTheTurnException).

Commandes utiles

./gradlew build            # build complet
./gradlew test             # tests (JUnit5 via Kotest)
./gradlew ktlintCheck      # lint
./gradlew ktlintFormat     # auto-format
./gradlew buildFatJar      # jar exécutable "all-in-one" (utilisé par le Dockerfile prod)

# Dépendances seules (Postgres, RabbitMQ, Træfik, pgAdmin...) pour lancer l'app en local hors docker
docker compose -f docker/docker-compose-dev.yaml up -d

# Stack de test façon CI
docker compose -f docker/docker-compose-test.yaml up -d
# ou directement (comme en CI) :
docker compose -f docker/docker-compose-test.yaml run tests

# Stack complète en prod
docker compose -f docker/docker-compose-prod.yaml -p event-demo up -d

URLs en dev (voir doc/installation.md, nécessite Træfik + résolution des *.traefik.me) : API sur http://api.traefik.me/, dashboard Træfik, pgAdmin et RabbitMQ management exposés via des sous-domaines traefik.me.

Conventions de code

  • ktlint en mode ktlint_official + règles standard et experimental activées (voir .editorconfig), indentation 2 espaces, virgules finales (trailing commas) systématiques, wrapping forcé des expressions/signatures multi-lignes.
  • Fins de ligne forcées en LF (.gitattributes), sauf gradlew.bat en CRLF.
  • Code et identifiants en anglais.
  • Style Kotlin idiomatique/fonctionnel : fold, let, apply, when exhaustifs, classes/interfaces scellées (sealed class/sealed interface) pour modéliser états et événements plutôt que des enums avec des champs optionnels.

Pièges connus / choses à savoir avant de toucher au build ou à la CI

  • MockK/ByteBuddy en Docker : l'auto-attach dynamique de MockK échoue dans les conteneurs (le handshake SIGQUIT de l'AttachListener JVM time-out). Le build.gradle.kts charge donc l'agent byte-buddy-agent de façon statique via -javaagent pour les tâches Test, afin que MockK détecte l'instrumentation déjà présente et saute l'attach dynamique. Ne pas retirer ce bloc sans repenser l'exécution des tests en Docker.
  • Secret Postgres en CI : docker/postgresql.secret est généré à la volée par le workflow GitHub Actions s'il n'existe pas (echo -n "changeit" > docker/postgresql.secret) — normal, pas un fichier à committer.
  • Les tests « officiels » de la CI tournent dans Docker, pas directement via ./gradlew test sur l'hôte — en cas de comportement différent entre local et CI, vérifier d'abord les variables d'environnement/versions du docker-compose-test.yaml.

Historique récent (pour contexte)

Le projet a connu un « Massive refactor to build the V2 » (commit e2d7942) : passage d'une architecture par couches techniques plates (adapter/presenter/domain) à l'organisation actuelle par bounded context (auth/game) avec 3 couches hexagonales chacune.