Andreas Dahm 342ce1bea1
Build, Test & Push Core Service / quality-check (push) Successful in 18s
Build, Test & Push Core Service / docker (push) Successful in 2m2s
Small fix
2026-05-12 09:50:47 +02:00
2026-05-12 09:50:47 +02:00
2026-04-19 09:37:47 +02:00

To Be Played (TBP) - Core Service

This is the Core Service for the "To Be Played" project. It acts as the central modulith, handling all business logic in vertical micro-service ready slices.

🚀 Tech Stack

  • Language: Java 25
  • Framework: Spring Boot 4.1.x
  • Security: Spring Security & JSON Web Tokens (JWT)
  • Database: PostgreSQL 17
  • Migrations: Flyway
  • Observability: Spring Boot Actuator & Micrometer (Prometheus)
  • Build Tool: Maven Wrapper (mvnw)
  • CI/CD: Gitea Actions (Forgejo)

🏗️ Architecture (Clean Architecture)

The project strictly follows a Domain-Driven / Clean Architecture (Ports & Adapters) approach to ensure high maintainability and testability. The codebase is divided into three main layers to prevent framework lock-in and keep business logic isolated.

Layer Breakdown

  1. API / Web Layer (api)

    • Contains REST endpoints, DTOs (Request/Response records), and Global Exception Handlers.
    • Uses ApiMapper to map between DTOs and Domain Models.
    • Rule: This layer delegates to the Service layer and never accesses persistence directly.
  2. Business / Core Layer (model, service, exception)

    • The heart of the application. Contains pure Java classes (Models) and Business Logic (Services).
    • Throws specific business exceptions (e.g., InvalidCredentialsException).
    • Rule: This layer has no dependencies on the API or Persistence layer. It defines interfaces that the persistence layer must implement.
  3. Persistence Layer (persistence)

    • Handles database operations. Contains JPA Entities (UserEntity), Spring Data Repositories, and the EntityMapper.
    • Implements the interfaces defined in the Domain layer.
    • Rule: Entities never leak into the API layer.

Architectural boundaries are enforced via ArchUnit (src/test/java/core_service/architecture/ArchitectureTest.java), which runs as part of the standard ./mvnw test quality gate. Abstract test bases live under core_service.shared.test (AbstractUnitTest, AbstractIntegrationTest, AbstractPersistenceTest, AbstractWebMvcTest, AbstractArchitectureTest).

Core-specific structure note. Unlike auth-service, Core keeps JPA @Entity classes in ..domain.. (rather than a separate ..persistence.entities.. package) and repository interfaces in ..repo... The ArchUnit rules are adapted to this layout. See "Roadmap / Open Topics" below for the planned tightening.

⚙️ Local Development

Running the application locally is fully automated via Spring Boot's Docker Compose Support.

Prerequisites

  • Java 25 installed
  • Docker & Docker Compose running

How to start

  1. Simply run the application from your IDE (e.g., IntelliJ) via the CoreServiceApplication.java main class.
  2. Spring Boot will automatically detect the compose.yaml and spin up a local PostgreSQL container (tbp_db_local) on port 5432.
  3. Flyway will automatically apply all database migrations located in src/main/resources/db/migration.
  4. The application will be available at http://localhost:8090.

To stop the environment and clean the database volume (if needed):

docker compose down -v

🔐 API Versioning

All API endpoints utilize Query Parameter Versioning to keep URLs clean and manageable. Example: GET /api/users/me?v=1

🐳 CI/CD Pipeline

The project uses Gitea Actions for Continuous Integration and Deployment.

Quality Gate: Runs ./mvnw clean verify to ensure tests and builds pass.

Docker Build: Packages the application using a multi-stage Dockerfile.

Registry: Pushes the lightweight Alpine-based image to the private Gitea container registry (git.andreas-dahm.eu/lobo_corp/tbp-core-service:latest).

📈 Observability

Metrics and health checks are exposed out-of-the-box via Spring Boot Actuator:

Health: http://localhost:8090/actuator/health

Prometheus Metrics: http://localhost:8090/actuator/prometheus

🗺️ Roadmap / Open Topics

Tracking known work that is not yet implemented. Each item is independently actionable.

Architecture / ArchUnit follow-ups

Currently active rules cover: cycles, controller→repository direction, repository naming, controller/service annotations, shared/configuration isolation, and a relaxed slice rule. The following rules are intentionally not yet enforced because they would fail today; addressing them is a separate refactor.

  • Strict module independence. library currently imports from steam_adapter (e.g. SteamApiService, response DTOs). The ArchUnit rule exempts that edge via ignoreDependency(library.., steam_adapter..). Long-term: either move steam_adapter out of modules (into core_service.adapters.steam or core_service.shared.steam) to make its role as a shared outbound port explicit, then enable strict notDependOnEachOther() without exemptions.
  • "DTOs only in ..api.." rule. Steam adapter response records currently live in ..api.dto.. and are consumed by library services. Move them to core_service.modules.steam_adapter.model (or .port) so they read as domain types, not API DTOs, then re-enable the strict rule.
  • "Entities only in ..persistence.." rule. Skipped because Core mixes entities with the domain package. Decide whether to (a) accept that pattern and keep the rule off, or (b) split entities into ..repo.entity.. and re-enable the rule for consistency with auth-service.

Security

  • Drop the shared JWT_SECRET. Done. Core now holds only JWT_PUBLIC_KEY and verifies with the Ed25519 public key via Jwts.parser().verifyWith(publicKey).build(). JwtConfig.secret removed.
  • Add a JwtServiceTest. Done. src/test/java/core_service/configuration/security/JwtServiceTest.java covers extract / valid / wrong-key paths with an Ed25519 fixture.
  • Consume a JWKS endpoint once Auth exposes one (GET https://auth.tbp.andreas-dahm.eu/.well-known/jwks.json). Cache keys at startup, refresh on kid miss. Avoids redeploying Core when keys rotate. With the kid header now present in issued tokens (JWT_KID), this is unblocked.

Testing

  • First Steam adapter integration test. SteamApiServiceTest is a unit test; an integration test with WireMock (or Spring's MockRestServiceServer) would lock in the contract with the external Steam API and the Resilience4j retry/rate-limit configuration.
  • Persistence-layer tests. library/repo/*Repository has no AbstractPersistenceTest-based test today. Good first targets: UserGameRepository, UserSteamSyncRepository.
  • Controller WebMvc tests. LibraryController, PublicStatsController, SteamController have no AbstractWebMvcTest-based tests yet. The Auth project's *ControllerTest classes are the pattern to follow.

CI

  • Wire the ArchUnit + scaffolding into the existing Forgejo Actions quality gate. ./mvnw verify already runs them; confirm CI fails (not just warns) on architecture violations so drift is caught before merge.

🎯 Product Roadmap (Core-owned slices)

The full cross-service product roadmap lives in ToBePlayed-Deployment/README.md. This section lists only the slices that land in Core, in suggested execution order.

1. Per-game tracking — the namesake feature

Today user_game mirrors Steam ownership and playtime, nothing else. Closing this gap turns the library from a Steam mirror into an actual backlog tracker.

  • Add a status column on user_game (BACKLOG / PLAYING / COMPLETED / DROPPED / ON_HOLD / WISHLIST) with a Flyway migration. Default at import: playtime > 0 → PLAYING, otherwise BACKLOG.
  • Add rating (smallint 1–10, nullable), completed_at (timestamp, nullable), notes (text, markdown, server-side rendered or sanitised on read).
  • Endpoint: PATCH /api/library/games/{appid} for partial updates of these fields. Stick with query-parameter versioning (?v=1).
  • Endpoint: filter GET /api/library/games?status=BACKLOG (extend the existing pagination contract).
  • New module candidate or extend library — keep it in library until tracking grows its own queries / reports, then split.

2. Picker / "what to play next"

  • Endpoint: GET /api/library/picker?budget=<minutes>&mood=<tag>&count=3. Pure read, uses status + (eventually) HLTB hours + tags as filters. Randomised within filter set.
  • No new persistence yet; build it on top of (1).

3. HowLongToBeat integration

  • New module core_service.modules.hltb (or library/hltb if kept internal). External adapter pattern mirrors steam_adapter.
  • Schema: game_completion_time(appid, main_hours, plus_sides_hours, completionist_hours, fetched_at).
  • Scheduled job to backfill + refresh (low cadence: monthly).
  • Risk: HLTB has no official API; community libs scrape. Cache aggressively, degrade gracefully when missing.

4. Achievement aggregation (data already in place)

  • UserGameAchievement + GameAchievementSchema are already synced. Add aggregate endpoints:
    • GET /api/library/games/{appid}/achievements/summary → { unlocked, total, percent }.
    • GET /api/library/me/achievements/summary → library-wide unlocked / total / percent.
    • GET /api/library/me/achievements/closest-to-completion?limit=10.
  • Pure SQL views or projection DTOs; no new external calls.

5. Goals and digests

  • Schema: user_goal(user_id, year, target_games_completed, ...).
  • Endpoint: GET/PUT /api/library/me/goal?v=1.
  • Scheduled weekly digest job: aggregate per-user "played hours, backlog delta, status changes" for last 7 days. Push to Auth (which owns email) or to a notification queue.

6. Wishlist + sale alerts

  • Extend steam_adapter with a wishlist endpoint.
  • New module / adapter for IsThereAnyDeal price polling.
  • Scheduled job; threshold per-user; trigger an email via Auth.

7. Library enrichment

  • Genre / tag filters: Steam store tags exist in some Steam API responses — verify which calls return them and add to the cache.
  • DLC awareness: Steam's GetAppDetails returns DLC; persist a game_dlc(appid_base, appid_dlc) table and a GET .../games/{appid}/dlc endpoint.
  • Series / franchise grouping: needs IGDB integration (separate adapter, requires a free IGDB OAuth token).
  • Multi-platform support: bigger schema change. Decouple Game (platform-agnostic) from SteamGame (platform-specific). New source enum (STEAM, GOG, EPIC, MANUAL, ...). Defer until at least one non-Steam adapter is on the immediate horizon.

8. Public profile data

  • Endpoint: GET /api/public/users/{username}/profile returning currently-playing + recently-completed + total hours, gated by a profile_public flag (owned by Auth, mirrored or fetched on demand).
S
Description
No description provided
Readme
2.1 MiB
0 Stars 3 Watchers 0 Forks
Languages
Java 99.4%
PLpgSQL 0.5%