メインコンテンツへ

層構造

Astar のバックエンドは Spring Modulith で、core/ 配下に 47 個のドメインモジュール、shared/ 配下に 11 個の横断モジュールがあります。すべてのモジュールが同じ 3 層規約に従います。

core/{module}/ の 3 層

core/{module}/
├── api/{controller,dto,mapper}                        # HTTP 層
├── domain/{model,repository,service,port,event,config,exception}
└── infrastructure/
    ├── persistence/{table,repository,resolver}          # Exposed DB 層
    ├── adapter/        # Port の実装
    ├── config/         # @Configuration / @ConfigurationProperties
    ├── listener/        # @TransactionalEventListener / @Async — domain/ には置かない
    └── scheduler/        # @Scheduled — domain/ には置かない
  • api/: HTTP の窓口。Controller は Service に委譲するだけで、ビジネスロジックを持たない。DTO は kotlinx.serialization で(詳細は 規約 参照)。
  • domain/: ビジネスロジックの本体。model/ は不変 data class、repository/ はドメイン型だけを話す interface(実装は infrastructure 側)、service/ がユースケースとトランザクション境界を持つ。
  • infrastructure/: 技術的関心事の実装。DB アクセス (Exposed)、外部サービス連携 (Port の実装)、非同期処理 (@Async リスナー、@Scheduled ジョブ) はすべてここに閉じ込める。

依存の向きは api → domain → infrastructure の一方向です。domain/infrastructure/ の実装クラスを知らず、domain/repository の interface 越しにしか DB を触りません。この向きが逆転する典型例(Controller が Repository を直接呼ぶ、Controller@Transactional を書く)は現状 ArchUnit ルールが存在せず CI で検知されないので、レビューで見る必要があります。

参照実装: core/tag/

一番小さく、規約に最も忠実な完全実装が core/tag/ です。迷ったらまずここを読んでください。

core/tag/
├── api/
│   ├── controller/TagController.kt
│   └── dto/
│       ├── TagCreateRequest.kt
│       ├── TagDto.kt
│       ├── TaggableDto.kt
│       └── TagUpdateRequest.kt
├── domain/
│   ├── exception/TagNotFoundException.kt
│   ├── model/
│   │   ├── Tag.kt
│   │   ├── Taggable.kt
│   │   └── TaggableType.kt
│   ├── repository/
│   │   ├── TaggableRepository.kt        # interface — ドメイン型のみ
│   │   └── TagRepository.kt
│   └── service/
│       ├── TaggingService.kt
│       └── TagService.kt
└── infrastructure/
    ├── adapter/TagTenantResolutionAdapter.kt
    └── persistence/
        ├── repository/
        │   ├── TagExposedRepositoryImpl.kt      # TagRepository の実装
        │   └── TaggableExposedRepositoryImpl.kt
        ├── resolver/TagIdResolver.kt
        └── table/
            ├── TagsTable.kt                       # Exposed table object = カラム定義の SSoT
            └── TaggablesTable.kt

TagRepositorydomain/repository/)は interface で Tag などのドメイン型しか登場しません。実装クラス TagExposedRepositoryImplinfrastructure/persistence/repository/)が TagsTable(Exposed table object)を使って実際の SQL を発行します。この分離のおかげで、domain/ 側のコードは「DB が Exposed である」ことすら知らずに書けます。

shared/ の位置づけ

shared/ は特定モジュールに属さない横断的な関心事です。api/ application/ domain/ exception/ extension/ infrastructure/ testing/ toolcontext/ util/ validation/ workflow/ の 11 サブディレクトリがあり、たとえば EntityId<T>(型安全な ID ラッパー)、RLS 機構一式(RLSInterceptorRlsDispatchersRlsTransactions)、WebMvcConfig(kotlinx.serialization 用の HTTP メッセージコンバータ設定)などがここに置かれています。モジュール固有のロジックを shared/ に置かない — 2 つ以上のモジュールが本当に必要とするものだけがここに来ます。

モジュールをまたぐ依存

モジュール間の呼び出しは基本的に避け、必要な場合は ModuleBoundaryTest.INTENTIONAL_CROSS_DOMAIN_TARGETS に明示的に登録されたパスのみ許可されます。新しいモジュール間依存を追加する前に、既存の登録例を確認してください。

core/organize/ のように、単一モジュール内をさらに機能パッケージ(extraction/citation/review/publicaccess/interview/convert/workflow/pack)に分割し、パッケージ間の許可された依存だけを ArchUnit(OrganizeFeatureBoundaryTest)で固定しているケースもあります。モジュールが育つと、この「モジュール内サブ境界」パターンが選択肢になります。

最初に踏みやすい choke point

backend/CLAUDE.md の Choke Points 表(全量)から、新人がまず遭遇しやすいものを抜粋します。「似た機能を作る前に、まずこれで足りないか確認する」ためのリストです。

やりたいこと使うべき choke point自分で書いてはいけないもの
ドキュメント/フォルダの作成・更新・削除StorageAwareDocumentServicecore/editor/domain/service/NAS と DB の整合を取る CUD ロジックの自前実装
レコード作成RecordService.persistNewRecordscore/table/テーブルへの直接 INSERT・バリデーションのバイパス
認可判定AuthorizationService + CustomMethodSecurityExpressionRoot の primitive (canAccessResource 等)Service 層での手書き権限チェック
外部 URL へのアクセス (AI fetch / mail / bot 等)EgressGuard / SsrfGuard / WebhookUrlValidator(用途別に使い分け)HttpClient での直接リクエスト

これらはすべて ArchUnit テストや専用ガードで守られています。似た機能を追加する前に「既存の choke point で表現できないか」を先に確認するのが最短ルートです。