3.7 KiB
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, andenumdefinitions - 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
modules/apiis a regular Gradle subproject already declared insettings.gradle.kts.- Consuming modules add
implementation(project(":modules:api"))— nothing else. - Because
modules/apihas no Quarkus BOM, consuming modules must not re-export Quarkus transitive dependencies through it. - 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 ofmodules/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.