Files
NowChessSystems/docs/adr/ADR-002-api-shared-models.md
T

3.7 KiB
Raw Blame History

ADR-002: Shared-Models Library (modules/api)

Status

Accepted

Context

NowChessSystems is a microservice platform. As soon as two or more services need to exchange data — whether through REST, messaging, or internal function calls — they must agree on common data types. Without a shared home for those types, the same case class (e.g. Square, Move, GameState) is duplicated in every module, diverges over time, and causes silent serialisation mismatches at runtime.

The core module currently owns the chess engine logic. Future modules (matchmaking, game history, user management, notation export, etc.) will all need to refer to the same chess domain vocabulary. A cross-cutting place to hold that vocabulary is therefore required before any second service is built.

Decision

We introduce modules/api as a shared-models library: a plain Scala 3 library (no Quarkus, no Jakarta, no persistence) that contains only:

  • Pure Scala 3 data types: case class, sealed trait, and enum definitions
  • Value objects that model the chess domain (pieces, colors, squares, moves, game state)
  • Cross-service API envelope types (ApiResponse[A], ApiError, Pagination)
  • Minimal player/user identity stubs (IDs and display names only)

Every service module that needs these types declares:

implementation(project(":modules:api"))

in its own build.gradle.kts. The modules/api module itself carries no runtime dependencies beyond the Scala 3 standard library.

Package layout

de.nowchess.api
├── board           Color, PieceType, Piece, File, Rank, Square
├── game            CastlingRights, GameState, GameResult, GameStatus
├── move            MoveType, Move, PromotionPiece
├── player          PlayerId, PlayerInfo
└── response        ApiResponse, ApiError, Pagination

What belongs in modules/api

Belongs Does NOT belong
case class, sealed trait, enum for chess domain Quarkus @ApplicationScoped beans
API envelope types (ApiResponse, ApiError) Jakarta Persistence entities (@Entity)
Player identity stubs (ID + display name) REST resource classes
FEN/board-state representation types Business logic, engine algorithms
Pure type aliases and value objects Database queries or repositories

The rule of thumb: if a type carries a framework annotation or requires I/O to produce, it does not belong in modules/api.

How other modules depend on it

  1. modules/api is a regular Gradle subproject already declared in settings.gradle.kts.
  2. Consuming modules add implementation(project(":modules:api")) — nothing else.
  3. Because modules/api has no Quarkus BOM, consuming modules must not re-export Quarkus transitive dependencies through it.
  4. If a future module needs JSON serialisation, it adds its own JSON library (e.g. circe, jsoniter-scala) as a dependency and derives codecs for the shared types there — codec derivation stays out of modules/api.

Consequences

Positive

  • Single source of truth for all chess domain vocabulary.
  • Adding a new microservice requires only one implementation(project(":modules:api")) line — no copy-paste of types.
  • The library is fast to compile (no framework processing) and cheap to test in isolation.
  • Enforces a strict boundary: if a type needs a framework annotation it is forced into the correct service module.

Negative / Risks

  • Any breaking change to a shared type (rename, field removal) is a cross-cutting change that touches every consuming module simultaneously.
  • Developers must resist the temptation to add convenience methods or logic to these types; discipline is required to keep the library pure.