docs(readme): add the architecture overview
Build, Test & Push Core Service / quality-check (push) Successful in 53s
Build, Test & Push Core Service / docker (push) Successful in 1m20s

This commit is contained in:
lobo committed 2026-09-26 07:37:44 +02:00
1 parent 7663e09a8c
commit b350de1c79
1 file changed
+67 -5
+67 -5
View File
@@ -71,18 +71,80 @@ accepted only by `GET /api/public/profiles/library` in the `X-Profile-Ticket` he
## Architecture
A modular monolith. Each module under `core_service.modules` is a vertical slice that could move to its own
service later:
service later.
```mermaid
flowchart LR
Browser["Frontend in the browser"]
subgraph CoreService["ToBePlayed-Core"]
Security["configuration.security<br/>JwtAuthenticationFilter"]
subgraph Library["library"]
LibApi["api"] --> LibService["service"] --> LibRepo["repo"]
end
Stats["stats"]
Steam["steam_adapter"]
Igdb["igdb_adapter"]
LibService --> Steam
LibService --> Igdb
end
Browser -->|"tbp_access cookie"| Security
Security -->|"JWKS by kid"| Auth["ToBePlayed-Auth"]
Steam --> SteamApi["Steam Web API"]
Igdb --> IgdbApi["IGDB API"]
Igdb -->|"OAuth token"| Twitch["Twitch"]
LibRepo --> Db[("PostgreSQL tbp_core")]
Stats --> Db
```
| Module | Content |
|---|---|
| `library` | Sync, backlog, playtime history, wishlist, public profile data |
| `library` | Sync, backlog, manual games, playtime history, wishlist, achievements, public profile data |
| `stats` | Public numbers for the landing page |
| `steam_adapter` | Steam Web API client |
| `igdb_adapter` | IGDB client (Twitch OAuth) |
| `igdb_adapter` | IGDB client (Twitch OAuth): time to beat, game tags for moods, game search |
Inside a module: `api` (controllers, DTOs), `service` (business logic), `domain` (JPA entities) and `repo`
(Spring Data repositories). ArchUnit enforces cycle freedom, controller-to-repository direction, naming and module
isolation in `src/test/java/core_service/architecture/ArchitectureTest.java`.
(repositories). `src/test/java/core_service/architecture/ArchitectureTest.java` checks these rules on every build:
- Modules do not depend on each other. Only `library` may use `steam_adapter` and `igdb_adapter`.
- No cycles between `modules`, `shared` and `configuration`.
- `api` never uses `repo`, and `repo` never uses `api`.
- Every class in `repo` ends with `Repository`.
- `shared` never uses `modules`, and `modules` never use `configuration`.
- A `*Service` class in a module's `service` package has `@Service`. A `*Controller` class in `api` has
`@RestController`, `@ControllerAdvice` or `@RestControllerAdvice`.
### Sync flow
1. `GET /api/library/games`, `/games-count` and `/picker` call `LibrarySyncService.syncIfStale`. It starts a sync
when the last one is older than 24 hours, older than 15 minutes after a failure, or was for another Steam ID.
`POST /api/library/sync` starts one at once, but at most every 60 seconds (else `429` with `Retry-After`).
2. A nightly job (23:55, Europe/Berlin) syncs every account used in the last 14 days, one after the other.
3. `LibrarySyncWorker` runs in the background. `user_steam_sync` lets only one run per user start.
4. The worker loads the wishlist and the owned games from Steam. It updates `steam_game` and `user_game`, adds
the playtime growth of the day to `user_game_playtime_day`, and soft-deletes Steam games the user no longer
owns. When Steam returns no games, the library stays as it is.
5. `TimeToBeatService` and `MoodService` ask IGDB for missing or old time-to-beat and mood data, for Steam games
and manual games.
6. After the commit, `LibrarySyncedEvent` starts `AchievementSyncService` in the background.
The frontend polls `GET /api/library/sync-status` until the run ends.
### Manual games next to Steam games
A game the user adds by hand comes from IGDB (`ManualGameService`). Steam games and manual games share
`steam_game` and `user_game` and one key, the column `appid` (see `GameKeys`):
- `appid > 0`: the Steam appid. A manual game gets it when IGDB knows the game on Steam.
- `appid < 0`: minus the IGDB id, for a game that is not on Steam. Steam appids are always positive, so the two
never meet.
`user_game.source` (`GameSource`) says who owns the row. The sync never changes or removes a `MANUAL` row, with
one exception: when the user later owns the game on Steam under the same appid, the sync takes the row over: it becomes
`STEAM`, and status, rating and notes stay. Only `MANUAL` rows can be edited (`PUT /api/library/games/{appid}/manual`)
or deleted (`DELETE /api/library/games/{appid}`). Time to beat uses the Steam appid for positive keys and the IGDB
id for negative keys. Search and add check both keys of a game. A game added under minus its IGDB id can still get
a second row when IGDB learns its Steam appid later and the user owns it on Steam.
## Run locally