規約
このページの内容は docs/backend/conventions.md(詳細版)と backend/CLAUDE.md(要約版)が正本です。ここでは新人がまず知っておくべき部分を抜き出します。
番人 と 自衛
正本ドキュメントの規約には、それぞれ次のタグが付いています。
- 番人: ArchUnit テスト・lint・deny 設定などが機械的に違反を検知する。破ると CI(または
/backend-test-reportsの guardTest)が落ちる。 - 自衛: 破ってもビルドは通る。検知するのはコードレビューだけ。「機械が守ってくれないから軽視していい」ではなく、むしろレビューでより厳しく見るべき、という意味のタグです。
このページでも同じタグを使います。新人のうちは 自衛 タグが付いた規約ほど「なぜこれが規約になっているか」の背景(多くは実インシデント)まで読んでおくと、レビューで指摘される前に気付けます。
命名規約
| 種類 | パターン | 例 |
|---|---|---|
| Controller | {Entity}Controller | TagController |
| Service | {Entity}Service | TagService |
| Exposed Table | {Entity}sTable(object、persistence/table/ 配下) | TagsTable |
| Exposed Repo 実装 | {Entity}ExposedRepositoryImpl | TagExposedRepositoryImpl |
| Config interface | {Concern}Config(domain/config/ 配下) | AICreditConfig |
| Mapper | {Entity}Mapper(接頭辞なし) | UserMapper |
| Public ID (UUID) | {entity}Id | tenantId, userId |
| Internal ID (BIGINT) | internal{Entity}Id | internalTenantId |
EntityId<T>(型安全な ID ラッパー)はドメインモデルの ID フィールドすべてに必須です。生の UUID フィールドを直接持たせません。
ドメイン駆動命名
新しい概念(フィールド・enum・フラグ・クラス)に名前を付ける前に、まず既存コードで同種の概念がどう呼ばれているかを grep で確認します。Astar の命名は業務用語(法律事務所のドメイン)に寄せてあり、汎用的すぎる名前や独自の略語・prefix は避けます。たとえばリソースの所有関係を表すなら既存の tenantId / createdByUserId の命名パターンに合わせ、「今回だけの」新しい命名規則を発明しないでください。表記ゆれ(同じ概念に複数の呼び方が並立する状態)はレビューで指摘対象です。
DTO 規約
DTO は kotlinx.serialization の @Serializable、Jackson ではありません。HTTP メッセージコンバータは KotlinSerializationJsonHttpMessageConverter(shared/infrastructure/config/WebMvcConfig.kt)1 本だけが登録されています。
- ワイヤー上のキー名を変えたい場合は
@SerialNameを使う。プレーンな kotlinx のリネーム +ignoreUnknownKeys = trueの組み合わせは、エラーにならずそのフィールドを黙って読み捨てるので危険です。 - 自衛
api/dto/*.ktの一部(core/devicelog/api/dto/DeviceLogDtos.ktやcore/ai/api/dto/AiRunDtos.ktなど)には現時点で@Serializableが付いていないファイルが約 10 本残っています。ガードは存在しません。もしこれらの DTO クラスが将来 Controller の戻り値型として使われると、コンパイルは通ったまま実行時にエンコードで例外になります。新しい DTO を書くときは@Serializableを付け忘れないでください。 api/dtoの中でBigDecimalを直接使わない — contextual なBigDecimalSerializer型を使います 番人:ApiDtoWireFormatTest。
Request/Response の命名
| パターン | 例 | 備考 |
|---|---|---|
{Entity}CreateRequest | TagCreateRequest | Create{Entity}Request は禁止 |
{Entity}UpdateRequest | TagUpdateRequest | 同上 |
{Entity}Response | TagResponse | |
PageResponse<T> | PageResponse<RecordResponse> | ページネーションあり |
{Entity}ListResponse | TableListResponse | ページネーションなし・件数表示 |
raw List<T> | List<TagResponse> | タグ・ロールなど小規模固定セット |
独自のページネーション形状を新しく作らないでください — 上記 3 パターンのどれかに当てはめます。
トランザクション規約(tx-poison doctrine)
Write メソッドは @Transactional、Read メソッドは @Transactional(readOnly = true)。ここまでは一般的な Spring の作法どおりです。Astar 特有なのは次の一点です。
try/catch や runCatching で REQUIRED トランザクション越しの呼び出しを囲んでも、共有トランザクションは保護されません。 例外が一度でも投げられると、その時点でトランザクションは "rollback-only" にマークされ、最終的な commit は UnexpectedRollbackException を投げます。catch していても関係ありません。
// NG: notifyService が例外を投げた時点で共有 @Transactional は汚染される。
// ここで catch していても、最後の commit が UnexpectedRollbackException で落ちる。
@Transactional
fun createRecord(...): Record {
val record = recordRepository.save(...)
runCatching { notifyService.notify(record) }
return record
}
// OK: ベストエフォートのサブ処理は RlsTransactions.requiresNew で
// 独立したトランザクションに隔離する。呼び出し元の RLS context も
// 新しいコネクションへ引き継がれる。
@Transactional
fun createRecord(...): Record {
val record = recordRepository.save(...)
runCatching { rlsTransactions.requiresNew { notifyService.notify(record) } }
return record
}
RlsTransactions.requiresNew { }(shared/infrastructure/security/rls/RlsTransactions.kt)が、通知・ログ・キャッシュウォームのようなベストエフォート処理を隔離する唯一の正しい方法です。素の @Transactional(REQUIRES_NEW) は凍結されていて 番人: RequiresNewTransactionArchitectureTest、直接使うと CI が落ちます。仮に使えたとしても RLS の GUC(テナントコンテキスト)を新しいコネクション上で失うため、RlsTransactions 経由でない限り RLS チェックが壊れます。
自衛 runCatching で REQUIRED を囲むパターン自体にはガードが一切なく、社内で最も繰り返し発生してきたバグクラスです。「catch しているから安全」という直感は、このコードベースでは通用しません。
参照
- 詳細版・全パターン: リポジトリの
docs/backend/conventions.md - 要約版: リポジトリの
backend/CLAUDE.md