層構造
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
TagRepository(domain/repository/)は interface で Tag などのドメイン型しか登場しません。実装クラス TagExposedRepositoryImpl(infrastructure/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 機構一式(RLSInterceptor・RlsDispatchers・RlsTransactions)、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 | 自分で書いてはいけないもの |
|---|---|---|
| ドキュメント/フォルダの作成・更新・削除 | StorageAwareDocumentService(core/editor/domain/service/) | NAS と DB の整合を取る CUD ロジックの自前実装 |
| レコード作成 | RecordService.persistNewRecords(core/table/) | テーブルへの直接 INSERT・バリデーションのバイパス |
| 認可判定 | AuthorizationService + CustomMethodSecurityExpressionRoot の primitive (canAccessResource 等) | Service 層での手書き権限チェック |
| 外部 URL へのアクセス (AI fetch / mail / bot 等) | EgressGuard / SsrfGuard / WebhookUrlValidator(用途別に使い分け) | HttpClient での直接リクエスト |
これらはすべて ArchUnit テストや専用ガードで守られています。似た機能を追加する前に「既存の choke point で表現できないか」を先に確認するのが最短ルートです。