ライブラリガイド
Cats、Circe、doobieなどはKamaeのドメイン規約を補助するライブラリである。トピック別リファレンス(エラーハンドリング、境界防御など)と矛盾する場合は、そちらを優先する。
ここでは「よくある組み合わせ」とデフォルトの置き場所をまとめる。個別の設計判断は エラーハンドリング、境界防御、ドメインモデリング、PII 保護 を参照する。
| 用途 | ガイド付きライブラリ | 検出のみ(ローカル慣習の参考) |
|---|---|---|
| エフェクト | cats-core、cats-effect、zio | monix、scalaz |
| JSON | circe | Play JSON、jsoniter-scala |
| 設定 | pureconfig | caliban config、Typesafe Config 直読み |
| SQL / ORM | doobie、slick | Quill、skunk |
| ストリーム | fs2 | Akka Streams、Pekko Streams |
| 検証 / newtype | refined | newtype、手書き opaque type |
| PII / シークレット | opaque credential wrapper(本ガイド secrets) | vault 連携、環境変数直読み |
| テスト | scalacheck、munit | ScalaTest、specs2 |
cats または cats-effect があるとき:
- ユースケースtraitに
Monad、Functor、ApplicativeError制約を適切に使う - 十分な理由がなければドメイン遷移を
F[_]から解放する - アダプター境界で
attempt/handleErrorWithによりエラーをマップする - I/Oには
IO/Fの遅延を優先し、flatMap内でblockingなしにブロックしない
エラーチャネルでは、純粋ドメインコードの Either と、アプリケーションコードの F[Either[E, A]] または ApplicativeError[F, E, *] は、一貫して使えばどちらも許容される。
| スタック | パターン | トピックガイド |
|---|---|---|
cats-effect + ポート | リポジトリ trait は F[_]、実装は IO | アプリケーション配線 |
ApplicativeError + ユースケース | ビジネス失敗を型付きエラーで表現 | エラーハンドリング |
Either + ドメイン | 遷移は純粋 Either、ユースケースが fromEither | 状態遷移 |
ZIOがあるとき:
- ユースケースを
ZIO[Env, UseCaseError, A]でモデルする - ドメイン遷移は純粋に保ち、
ZIO.fromEitherで呼ぶ - レイヤーはcomposition rootのみで提供する
- ビジネス失敗には
Throwableではなく型付きエラーをエラーチャネルに使う
ドメインパッケージは、プロジェクトがエフェクト型をアプリケーションコードと明示的に同居させない限り zio に依存しない。
| スタック | パターン | トピックガイド |
|---|---|---|
ZLayer + ポート | アダプター実装のみレイヤー化 | アプリケーション配線 |
ZIO + Either 遷移 | fromEither でドメインを呼ぶ | 状態遷移 |
CirceはJSON境界向けであり、ドメイン不変条件の権威にはしない。
DTO に Codec を付ける
Section titled “DTO に Codec を付ける”import io.circe.Decoder
final case class RequestDto(requestId: String, passengerId: String, status: String)
object RequestDto: given Decoder[RequestDto] = Decoder.derivedDecoder.derived はビジネスルールを検証しない。ネストしたフィールドのcodecも、implicit scopeに Decoder がない限り自動導出されない。
ドメイン型にはバリデータを使う
Section titled “ドメイン型にはバリデータを使う”DTOにデコードし、明示的な Either マッピングで変換する。検証がdecoderに埋め込まれテストされている場合を除き、不変条件を持つ型に Decoder[WaitingRequest] を避ける。
def decodeWaiting(dto: RequestDto): Either[BoundaryError, WaitingRequest] = for requestId <- RequestId(dto.requestId).left.map(BoundaryError.InvalidId.apply) passengerId <- PassengerId(dto.passengerId).left.map(BoundaryError.InvalidId.apply) _ <- Either.cond(dto.status == "waiting", (), BoundaryError.UnexpectedStatus(dto.status)) yield WaitingRequest(requestId, passengerId, requiresAccessibleVehicle = false)設定付き導出
Section titled “設定付き導出”snake_caseキー、デフォルト、判別子が必要なときは Configuration を提供し、configured derivationを使う:
import io.circe.derivation.Configuration
given Configuration = Configuration.default.withSnakeCaseMemberNames
object RequestDto: given Decoder[RequestDto] = Decoder.derivedConfigured和型と enum
Section titled “和型と enum”sealed familyには Codec.AsObject.derived が既知のsubtypeを自動導出する。単純なenum:
enum Status derives Decoder, Encoder: case Waiting, EnRoute外部制御のstatus文字列には明示的decoderを優先し、任意の文字列をドメインenumに受け入れない。
Play JSON
Section titled “Play JSON”Play JSONを使うプロジェクトでも境界ルールは同じ: DTOに Reads / Writes、その後にドメイン型への検証付き変換。Json.format 導出を不変条件の強制とみなさない。
| スタック | パターン | トピックガイド |
|---|---|---|
circe + DTO | Decoder → Either マッピング | 境界防御 |
circe + http4s | EntityDecoder で DTO、ハンドラでドメイン変換 | 境界防御 |
circe + イベント | 外向きイベント DTO のみ codec | 永続化、集約、イベント |
doobie
Section titled “doobie”doobieはSQLアダプター向けであり、ドメインモデリング向けではない。
Read / Write インスタンスはinfrastructureの行case classに置く。リポジトリポートから返す前に、明示的な Either マッパーで行をドメイン型にマップする。
トランザクションはアダプターに属する
Section titled “トランザクションはアダプターに属する”ドメイン遷移内ではなく、アダプターまたはユースケース境界で transact(xa) を使う。1コマンドの状態変更とoutbox挿入は同一トランザクションを共有する。
ConnectionIO を漏らさない
Section titled “ConnectionIO を漏らさない”リポジトリtraitはポートレベルで F[_](通常 IO)を使う。ConnectionIO はアダプター実装内に留める。
詳細は ORM アダプター を参照する。
プロジェクトがすでにSlickを標準とするとき、SQLアダプターに使う。
テーブル定義は infrastructure に留める
Section titled “テーブル定義は infrastructure に留める”Table サブクラス、DBIO、profile importをドメインモジュールから出す。リポジトリポートは F[_] とドメイン型のみを使う。
返す前にマップする
Section titled “返す前にマップする”RequestRow(相当)をアダプター内で、ORM アダプター と同じ検証マッパーでドメイン状態に変換する。
セッションとトランザクション
Section titled “セッションとトランザクション”db.run(...transactionally) をアダプターが所有する。Database や DBIO をユースケースに渡さない。
ドメインマッピング中のlazy loadや外部キー関係のナビゲーションを避け、必要な状態の列を明示的にクエリする。
FS2は読み取り側のストリームポート、outboxディスパッチ、プロジェクション向けに使う。
ストリームをドメインから出す
Section titled “ストリームをドメインから出す”ドメイン遷移は Either とイベントリストを返す。アダプターが永続化ログやoutboxテーブル上の Stream[F, A] を公開する。
ストリーム要素には型付きエラーを優先する
Section titled “ストリーム要素には型付きエラーを優先する”Stream[F, Either[StreamError, DomainEvent]] はマッパーとデコード失敗を明示的に保つ。メトリクスとデッドレターポリシーなしに handleErrorWith(_ => Stream.empty) で失敗を飲み込まない。
interruptWhen またはファイバキャンセルでストリームをコンパイルし、コンシューマ切断時にDBポーリングを止める。
詳細は ストリームと継続クエリ を参照する。
refined
Section titled “refined”eu.timepit.refined は境界または単一フィールド不変条件向けの検証付きプリミティブnewtypeに使う。検証メッセージをドメイン固有にする必要があるドメインモジュールでは、明示的 Either ファクトリ付きopaque typeを優先する。
- 形式ルール付きのconfigキー、クエリパラメータ、DTOフィールド(非空、UUID、正のInt)
- 段階的導入: 完全なドメインモデリング前にレガシー
String/Int列をラップする
使わないとき
Section titled “使わないとき”- 複数フィールドまたは状態依存ルール — ドメイン型と遷移を使う
- ORMマッピングがrefined述語を曖昧にする永続化集約ルート
import eu.timepit.refined.api.*import eu.timepit.refined.collection.NonEmptyimport eu.timepit.refined.refineEither
type NonEmptyString = String Refined NonEmpty
def parseRequestId(raw: String): Either[BoundaryError, NonEmptyString] = refineEither[NonEmpty](raw).left.map(_ => BoundaryError.EmptyId("request_id"))refined DTOフィールドを、アダプター境界で明示的エラー ADT付きopaqueドメインIDにマップする。境界防御、ドメインマクロ も参照する。
secrets
Section titled “secrets”完全なパターンは PII 保護 を優先する。本節は資格情報とAPIキー向けのScala固有デフォルトを扱う。
ドメインまたはユースケース層に生の String でシークレットを置かない。toString を制限したopaque type、あるいは生値を決してログしない専用wrapperを優先する。
final class ApiToken private (private val value: String): override def toString: String = "ApiToken(***)"
object ApiToken: def parse(raw: String): Either[BoundaryError, ApiToken] = if raw.trim.isEmpty then Left(BoundaryError.EmptyField("api_token")) else Right(new ApiToken(raw.trim))
extension (token: ApiToken) def expose: String = token.valueシークレット値の露出はHTTP / auth / payment境界の狭いアダプター関数(expose、value)に限定する。露出した値をerror ADTに含めない。
| スタック | パターン | トピックガイド |
|---|---|---|
| opaque secret + アダプター | auth モジュールのみ expose | PII 保護 |
| ログ | token フィールドをログしない。構造化 *** プレースホルダ | ロギングとメトリクス |
| PII vs secrets | 個人データは redacted 型、資格情報は secret wrapper | PII 保護 |
検出のみ: pureconfig のsecret loader — 境界で検証し、ドメインコード実行前にopaque型へマップする。
scalacheck
Section titled “scalacheck”プロジェクトがすでに依存している場合、またはプロパティテストが入力全体の法則を最も明確にカバーできる場合に使う。
Test スコープに置く。無効なドメイン状態を直接構築するより、publicコンストラクタを呼ぶgeneratorを優先する。
import org.scalacheck.Prop.forAllimport org.scalacheck.Gen
property("valid ids construct") { forAll(nonEmptyStringGen) { raw => RequestId(raw.trim).isRight }}generator設計、状態プロパティ、CI予算、regressionファイルは プロパティベーステスト を参照する。
pureconfig
Section titled “pureconfig”PureConfigは設定ファイルを読む。ドメインコマンドを読まない。
設定 case class は境界型
Section titled “設定 case class は境界型”デフォルトを明示的に文書化したcase classに設定をロードし、ドメイン型へ検証する。
シークレット
Section titled “シークレット”起動時ログされる平文configフィールドにシークレットを置かない。環境別secretプロバイダとredacting wrapperを使う。
境界防御 も参照する。
| スタック | パターン | トピックガイド |
|---|---|---|
pureconfig + 起動 | config case class → ドメイン検証 | 境界防御 |
pureconfig + secrets | 読み込み後すぐ opaque 型へ | PII 保護 |