コンテンツにスキップ

共通コーディング規約

本ドキュメントと合わせて以下を参照すること。

ドキュメント内容
ArchitecturePolicy_Common.md共通アーキテクチャ方針
AuthenticationSpecification_Common.md共通認証仕様
機能別コーディング規約(例:CodingConventions_Coordination_Sprint1.md)機能・フェーズ固有のパッケージ名・APIパス設計等

本ドキュメントは、Java Spring Boot 実装における共通コーディング規約を定義する。

Claude Code によるコード生成時は、本ドキュメントの内容を遵守し、可読性・保守性・一貫性のあるコードを生成すること。

本規約の対象は以下とする。

  • Java ソースコード
  • Spring Boot 関連コード
  • Handler(Primary Adapter)
  • UseCase
  • Domain Model(Entity・値オブジェクト)
  • Port(インターフェース)
  • Repository 実装(Secondary Adapter)
  • MyBatis Mapper(インターフェース + XML)
  • Exception
  • Unit Test / Integration Test
  • 設定ファイル

コード作成時は以下を基本方針とする。

  • 読みやすさを優先する
  • 責務を明確に分離する
  • 重複コードを避ける
  • 過度に複雑な設計を避ける
  • ヘキサゴナルアーキテクチャの構成を優先する
  • OpenAPI yaml と DB create SQL の定義を尊重する
  • 業務仕様が不明な場合は、勝手に仕様を追加せずコメントで仮定を明示する
  • コンパイル可能なコードを生成する
  • 未使用 import を残さない
  • TODO コメントを乱用しない
  • Google Java Format(Spotless)に準拠したフォーマットとする

Java 25 を前提とする。

Java 25 で利用可能な構文は使用してよいが、可読性を損なう書き方は避ける。

使用してよい例(var):

var result = createFeatureAUseCase.execute(cmd);

ただし、型が分かりにくくなる場合は var を使用しない。

避ける例:

var data = repository.findById(id).orElseThrow();

推奨例:

FeatureA featureA = repository.findById(id)
.orElseThrow(() -> new FeatureANotFoundException(id));

recordsealed interfaceswitch 式(パターンマッチング)・テキストブロック等は積極的に活用する。


ソースコードおよび設定ファイルは以下とする。

項目規約
文字コードUTF-8
改行コードLF
インデント半角スペース 2 つ(Google Java Format 準拠)
タブ文字使用禁止
1 行の文字数目安100 文字以内(Google Java Format 準拠)

フォーマットは ./gradlew spotlessApply で自動整形する。


パッケージ名はすべて小文字とする。

組織識別子は com.intent_exchange とする。機能識別子は機能ごとに定める(機能別コーディング規約ドキュメントを参照すること)。

推奨(パッケージ構成の例):

com.intent_exchange.<feature>.config
com.intent_exchange.<feature>.generated.api
com.intent_exchange.<feature>.generated.model
com.intent_exchange.<feature>.handler
com.intent_exchange.<feature>.handler.problem
com.intent_exchange.<feature>.handler.response
com.intent_exchange.<feature>.usecase
com.intent_exchange.<feature>.domain.model
com.intent_exchange.<feature>.domain.port
com.intent_exchange.<feature>.domain.exception
com.intent_exchange.<feature>.domain.common
com.intent_exchange.<feature>.infrastructure.persistence.postgres
com.intent_exchange.<feature>.infrastructure.persistence.postgres.mapper

禁止:

com.example.FeatureA.Handler
com.intent_exchange.<feature>App.Service

各パッケージには package-info.java を作成し、パッケージの責務を記述する。

機能ごとの具体的なパッケージ名は、機能別コーディング規約ドキュメントを参照すること。


クラス名は UpperCamelCase とする。

種別命名規約
Handler(Primary Adapter)XxxHandlerFeatureAHandler
UseCaseXxxUseCaseCreateFeatureAUseCase
Domain Entity(sealed 状態型)状態ごとに意味ある名称FlightPlan.Draft / FlightPlan.Approved
値オブジェクト意味ある名称FeatureAId / EntityValue
Port(Repository I/F)XxxRepositoryFeatureARepository
Port(TransactionManager)TransactionManagerTransactionManager
Repository 実装XxxRepositoryImplFeatureARepositoryImpl
MyBatis MapperXxxMapperFeatureAMapper
DB 行レコードXxxRecordFeatureARecord
レスポンス変換 MapperXxxResponseMapperFeatureAResponseMapper
ドメイン例外XxxExceptionFeatureANotFoundException
ConfigXxxConfigAppConfig
TestXxxTestCreateFeatureAUseCaseTest

メソッド名は lowerCamelCase とする。

処理内容が分かる名称を使用する。

推奨(UseCase):

public FeatureA execute(CreateFeatureACommand command) { ... }

推奨(Repository):

Optional<FeatureA> findById(FeatureAId id);
Optional<FeatureA> findByIdForUpdate(FeatureAId id);
List<FeatureA> findAll();
FeatureA save(FeatureA featureA);
void deleteById(FeatureAId id);

避ける例:

public FeatureA get(FeatureAId id) { ... }
public FeatureA exec(CreateFeatureACommand cmd) { ... }
public void process(FeatureAId id) { ... }

変数名は lowerCamelCase とする。

意味が分かる名称を使用し、過度な省略は避ける。

推奨:

FeatureA featureA = repository.findById(id)
.orElseThrow(() -> new FeatureANotFoundException(id.value()));
List<FeatureAResponse> responses = entities.stream()
.map(FeatureAResponseMapper::from)
.toList();

避ける例:

FeatureA f = repository.findById(id).orElseThrow();
List<FeatureAResponse> list = data.stream()
.map(FeatureAResponseMapper::from)
.toList();

ただし、ラムダ式や短いスコープでは一般的な短縮名を許容する。

entities.stream()
.map(e -> FeatureAResponseMapper.from(e))
.toList();

定数は以下の形式とする。

  • static final
  • 大文字スネークケース

例:

private static final String DEFAULT_CREATED_BY = "system";
private static final int DEFAULT_PAGE_SIZE = 20;

マジックナンバーや固定文字列を処理内に直接書かない。

避ける例:

if (request.getStatusCode() == 9) {
throw new IllegalArgumentException("invalid status");
}

推奨例(ただし状態は sealed interface で型として表現することを優先する):

// sealed interface で状態を型として表現する(アーキテクチャ方針 6.2 参照)
return switch (entity) {
case FlightPlan.Draft d -> "下書き";
case FlightPlan.Approved a -> "承認済み";
// ...
};

Handler は HTTP リクエスト/レスポンスの制御と UseCase 呼び出しのみを行う。

Handler に業務ロジックを書かない。

Handler では以下を実施する。

  • 生成 API インターフェース(XxxApi)の implements
  • 意味的 parse(基本型 → ドメイン値オブジェクトへの変換)
  • UseCase 呼び出し
  • ドメイン型 → 生成 DTO への変換(XxxResponseMapper を使用)
  • レスポンス返却

例:

@Validated
@RestController
public class FeatureAHandler implements FeatureAsApi {
private final CreateFeatureAUseCase createFeatureAUseCase;
private final GetFeatureAUseCase getFeatureAUseCase;
public FeatureAHandler(
CreateFeatureAUseCase createFeatureAUseCase,
GetFeatureAUseCase getFeatureAUseCase) {
this.createFeatureAUseCase = createFeatureAUseCase;
this.getFeatureAUseCase = getFeatureAUseCase;
}
@Override
public ResponseEntity<FeatureAResponse> createFeatureA(
CreateFeatureARequest request) {
var featureA = createFeatureAUseCase.execute(
new CreateFeatureACommand(request.getTitle()));
return ResponseEntity.status(HttpStatus.CREATED)
.body(FeatureAResponseMapper.from(featureA));
}
@Override
public ResponseEntity<FeatureAResponse> getFeatureA(Long id) {
return ResponseEntity.ok(
FeatureAResponseMapper.from(getFeatureAUseCase.execute(id)));
}
}

Handler では以下を禁止する。

// 禁止: Handler で Repository を直接呼び出す
private final FeatureARepository featureARepository;
// 禁止: Handler に業務ロジックを書く
if (request.getStatus() == 1) { ... }
// 禁止: ドメイン例外に HTTP ステータスを設定する
// (GlobalExceptionHandler の責務)
// 禁止: Handler ごとに try-catch を多用する
try { ... } catch (Exception e) { ... }
// 禁止: UseCase に HttpServletRequest を渡す
useCase.execute(request, httpServletRequest);

例外処理は GlobalExceptionHandler で実施する。


UseCase はユースケース単位の処理フローを担当する。

1 ユースケース = 1 クラス = 1 execute メソッド。

public class CreateFeatureAUseCase {
private static final Logger log = LoggerFactory.getLogger(CreateFeatureAUseCase.class);
private final FeatureARepository featureARepository;
private final TransactionManager transactionManager;
public CreateFeatureAUseCase(
FeatureARepository featureARepository,
TransactionManager transactionManager) {
this.featureARepository = featureARepository;
this.transactionManager = transactionManager;
}
public FeatureA execute(CreateFeatureACommand command) {
var featureA = transactionManager.execute(
() -> featureARepository.save(FeatureA.create(command.title())));
log.info("event=featureA.created id={}", featureA.id());
return featureA;
}
}

UseCase では以下を禁止する。

// 禁止: HTTP ステータスを UseCase で直接扱う
return ResponseEntity.ok(response);
// 禁止: HttpServletRequest に依存する
public FeatureA execute(HttpServletRequest request) { ... }
// 禁止: @Transactional を使う(TransactionManager を使う)
@Transactional
public FeatureA execute(...) { ... }
// 禁止: @Component/@Service を付与する
@Service
public class CreateFeatureAUseCase { ... }
// 禁止: io.micrometer / io.opentelemetry を import する
import io.micrometer.core.instrument.MeterRegistry;

Domain は Spring・MyBatis・Infrastructure に一切依存しない。

12.1 値オブジェクト(record + スマートコンストラクタ)

Section titled “12.1 値オブジェクト(record + スマートコンストラクタ)”
record FeatureAId(long value) {
public FeatureAId {
if (value <= 0) throw new InvalidFeatureAIdException();
}
}

12.2 状態を持つ集約(sealed interface + record)

Section titled “12.2 状態を持つ集約(sealed interface + record)”
/// 飛行調整ドメインモデル。sealed interface で状態ごとにクラスを分ける。
public sealed interface FlightPlan
permits FlightPlan.Draft, FlightPlan.Submitted, FlightPlan.Approved, FlightPlan.Rejected {
FlightPlanId id();
FlightRoute route();
record Draft(FlightPlanId id, FlightRoute route) implements FlightPlan {
public Draft {
if (id == null) throw new InvalidFlightPlanIdException();
if (route == null) throw new InvalidFlightRouteException();
}
public Submitted submit() {
return new Submitted(id, route);
}
// approve() は存在しない → Draft から直接承認不可(コンパイルエラー)
}
record Submitted(FlightPlanId id, FlightRoute route) implements FlightPlan {
public Approved approve() {
return new Approved(id, route);
}
public Rejected reject(String reason) {
return new Rejected(id, route, reason);
}
}
record Approved(FlightPlanId id, FlightRoute route) implements FlightPlan {}
record Rejected(FlightPlanId id, FlightRoute route, String reason) implements FlightPlan {}
}

Domain Model では以下を禁止する。

// 禁止: ORM アノテーションの付与
@Entity
@Table(name = "feature_a")
public class FeatureA { ... }
// 禁止: Bean Validation アノテーションの付与
@NotNull
@Size(max = 100)
private String title;
// 禁止: Spring の依存
@Autowired
private FeatureARepository repository;
// 禁止: Infrastructure を import する
import com.<org>.<feature>.infrastructure.persistence.postgres.FeatureARepositoryImpl;

Port インターフェースは domain.port パッケージに定義する。

/// 機能 A の Repository Port。
public interface FeatureARepository {
/// 指定 ID のエンティティを取得する。存在しない場合は `Optional.empty()` を返す。
Optional<FeatureA> findById(FeatureAId id);
/// 指定 ID のエンティティを排他ロックで取得する(状態遷移・競合制御用)。
Optional<FeatureA> findByIdForUpdate(FeatureAId id);
/// 全エンティティを取得する。
List<FeatureA> findAll();
/// エンティティを保存する(INSERT または UPDATE)。
FeatureA save(FeatureA featureA);
/// 指定 ID のエンティティを削除する。
void deleteById(FeatureAId id);
}

/// トランザクション制御の Port。Infrastructure から Spring に依存させる。
public interface TransactionManager {
/// アクションをトランザクション内で実行する。
<T> T execute(Supplier<T> action);
/// トランザクション属性を指定してアクションを実行する。
<T> T execute(TransactionOptions options, Supplier<T> action);
}

Repository 実装は infrastructure.persistence.postgres パッケージに配置する。

Port インターフェースを implements する。

/// PostgreSQL バックの FeatureARepository 実装。MyBatis を使用する。
public class FeatureARepositoryImpl implements FeatureARepository {
private final FeatureAMapper featureAMapper;
public FeatureARepositoryImpl(FeatureAMapper featureAMapper) {
this.featureAMapper = featureAMapper;
}
@Override
public Optional<FeatureA> findById(FeatureAId id) {
try {
return featureAMapper.findById(id.value()).map(this::toDomain);
} catch (DataAccessException e) {
throw new RepositoryException("failed to find featureA", e);
}
}
@Override
public FeatureA save(FeatureA featureA) {
try {
return toDomain(featureAMapper.upsert(toRecord(featureA)));
} catch (DataAccessException e) {
throw new RepositoryException("failed to save featureA", e);
}
}
// ドメイン型 ↔ DB 行レコードの変換は実装クラス内に閉じる
private FeatureARecord toRecord(FeatureA featureA) { ... }
private FeatureA toDomain(FeatureARecord record) { ... }
}

Repository メソッドは検索条件が分かる名前にする。

推奨:

Optional<FeatureA> findById(FeatureAId id);
Optional<FeatureA> findByIdForUpdate(FeatureAId id);
List<FeatureA> findByStatus(FeatureAStatus status);

避ける例:

Optional<FeatureA> getData(FeatureAId id);
boolean check(FeatureAId id);

MyBatis Mapper インターフェースと XML を infrastructure.persistence.postgres.mapper パッケージに配置する。

SQL は XML に記述する。パラメータは必ず #{} を使用する。

禁止:

<!-- 禁止: 文字列連結による SQL 組み立て -->
<select id="findByTitle" resultType="FeatureARecord">
SELECT * FROM feature_a WHERE title = '${title}'
</select>

推奨:

<select id="findById" resultType="FeatureARecord">
SELECT
<include refid="featureAColumns"/>
FROM feature_a
WHERE id = #{id}
</select>

共有する SELECT 本体は <sql> フラグメントで重複を避ける。


DB 行レコード(XxxRecord)は Infrastructure 層内にのみ存在する。

record クラスとして定義する。

record FeatureARecord(
long id,
String title,
String status,
@Nullable OffsetDateTime createdAt) {}

DB 行レコードを Domain 層・UseCase 層・Handler 層に持ち込まない。


ドメイン例外は domain.exception パッケージに定義する。

ビジネスルール違反は BusinessRuleException を継承する。

FeatureANotFoundException のような「対象データが存在しない」例外も BusinessRuleException を継承する。これは、対象データが存在しないことをシステムエラーではなくビジネスルール上の状態として扱い、HTTP 404 として表現するためである。

/// ビジネスルール違反の基底例外。
public class BusinessRuleException extends RuntimeException {
public BusinessRuleException(String message) {
super(message);
}
}
/// 対象エンティティが見つからない場合の例外。
// 「存在しない」はビジネスルール上の状態であるため BusinessRuleException を継承する(HTTP 404)。
public class FeatureANotFoundException extends BusinessRuleException {
public FeatureANotFoundException(long id) {
super("エンティティが見つかりません。id=" + id);
}
}
/// 無効な状態遷移が行われた場合の例外。
public class InvalidFeatureATransitionException extends BusinessRuleException {
public InvalidFeatureATransitionException(String message) {
super(message);
}
}

ドメイン例外には HTTP ステータスコードを含めない。

例外メッセージは、利用者または開発者が原因を理解できる内容にする。

避ける例:

throw new RuntimeException("error");

推奨例:

throw new FeatureANotFoundException(id.value());

17. 共通エラーレスポンス規約(RFC 9457 Problem Details)

Section titled “17. 共通エラーレスポンス規約(RFC 9457 Problem Details)”

API エラー時は RFC 9457 Problem Details 形式でレスポンスを返却する。

{
"type": "https://example.com/problems/not-found",
"title": "リソースが見つかりません",
"status": 404,
"detail": "指定された ID が存在しません。id=123",
"instance": "/api/v1/feature-a/entities/123"
}

バリデーションエラー(422)には errors 配列を含める。

{
"type": "https://example.com/problems/validation-error",
"title": "入力値が不正です",
"status": 422,
"errors": [
{ "field": "title", "message": "タイトルは必須です" }
]
}

ProblemDetail を各所で手組みせず、GlobalExceptionHandlerbuildProblem ヘルパーに集約する。


例外は @RestControllerAdviceGlobalExceptionHandler)で一元的に処理する。

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(FeatureANotFoundException.class)
public ResponseEntity<ProblemDetail> handleFeatureANotFoundException(
FeatureANotFoundException ex, HttpServletRequest request) {
return buildProblem(HttpStatus.NOT_FOUND, ProblemTypes.NOT_FOUND, ex.getMessage(), request);
}
@ExceptionHandler(BusinessRuleException.class)
public ResponseEntity<ProblemDetail> handleBusinessRuleException(
BusinessRuleException ex, HttpServletRequest request) {
return buildProblem(HttpStatus.UNPROCESSABLE_ENTITY, ProblemTypes.BUSINESS_RULE_VIOLATION,
ex.getMessage(), request);
}
private ResponseEntity<ProblemDetail> buildProblem(
HttpStatus status, URI type, String detail, HttpServletRequest request) {
// 共通 buildProblem ヘルパーの実装
...
}
}

Handler ごとに同じような try-catch を書かない。


19.1 OAS 由来のバリデーション(生成 DTO)

Section titled “19.1 OAS 由来のバリデーション(生成 DTO)”

OAS に記述した制約(minLengthmaxLengthpatternrequired 等)が生成 DTO に @NotNull@Size@Pattern として付与される。

Handler メソッドの @Valid でこれを起動する。

# OAS で制約を定義する例
title:
type: string
minLength: 1
maxLength: 255
pattern: '.*\S.*'

生成 DTO に手動で @NotBlank 等を追加しない(手編集禁止)。


基本型 → ドメイン値オブジェクトへの変換は Handler 層で行う。

@Override
public ResponseEntity<FeatureAResponse> createFeatureA(
CreateFeatureARequest request) {
// LocalDate → DueDate などの意味的 parse
var dueDate = DueDate.from(request.getDueDate());
var featureA = createFeatureAUseCase.execute(
new CreateFeatureACommand(request.getTitle(), dueDate));
return ResponseEntity.status(HttpStatus.CREATED)
.body(FeatureAResponseMapper.from(featureA));
}

19.3 Domain のスマートコンストラクタ

Section titled “19.3 Domain のスマートコンストラクタ”

Domain 値オブジェクトのコンストラクタ内で意味的制約を検証する(アーキテクチャ方針 6.2 参照)。

Bean Validation アノテーション(@NotNull@Min 等)はドメインオブジェクトに付与しない。


DB 参照が必要な整合性チェックは UseCase 層で行う。

例:

public FeatureA execute(CreateFeatureACommand command) {
return transactionManager.execute(() -> {
// 重複確認(DB 参照が必要なため UseCase で実施)
if (featureARepository.existsByTitle(command.title())) {
throw new FeatureADuplicateException("既に登録されています。");
}
return featureARepository.save(FeatureA.create(command.title()));
});
}

@Transactional は使用しない。

domain.port.TransactionManager を使用してトランザクションを制御する。

更新系・参照系ともに UseCase の execute メソッド内で TransactionManager を使う。

更新系:

return transactionManager.execute(() -> {
var featureA = repository.findByIdForUpdate(id)
.orElseThrow(() -> new FeatureANotFoundException(id.value()));
return repository.save(featureA.doSomething());
});

参照系:

return transactionManager.execute(
TransactionOptions.forReadOnly(),
() -> repository.findAll());

Handler 層・Infrastructure 層では原則として直接トランザクション制御を行わない。


Repository の戻り値が存在しない可能性がある場合は Optional を使用する。

推奨:

FeatureA featureA = featureARepository.findById(id)
.orElseThrow(() -> new FeatureANotFoundException(id.value()));

避ける例:

Optional<FeatureA> optFeatureA = featureARepository.findById(id);
if (optFeatureA.isPresent()) {
return optFeatureA.get();
}
return null;

原則として Optional.get() を直接使用しない。


全パッケージの package-info.java に JSpecify の @NullMarked を付与する。

null 許容箇所のみ @Nullable を付与する。

// package-info.java の例
@NullMarked
package com.<org>.<feature>.domain.model;
import org.jspecify.annotations.NullMarked;
// @Nullable の使用例
@Nullable OffsetDateTime createdAt()

戻り値として安易に null を返さない。対象データが存在しない場合は、空のコレクションや Optional を返すことを基本とする。業務ロジック上「対象データが必ず存在すべき」と判断する場合に限り例外を送出する。

避ける例:

public FeatureA findById(FeatureAId id) {
return null;
}

推奨例(業務上必ず存在すべきケース):

public FeatureA findById(FeatureAId id) {
return featureARepository.findById(id)
.orElseThrow(() -> new FeatureANotFoundException(id.value()));
}

戻り値のコレクションは null ではなく空リストを返す。

推奨:

return entities.stream()
.map(FeatureAResponseMapper::from)
.toList();

避ける例:

if (entities.isEmpty()) {
return null;
}

日時は以下の型を使用する。

用途Java 型
日付LocalDate
日時OffsetDateTime
時刻LocalTime

外部に出す日時(API レスポンス)は UTC 日時とする。

現在日時の取得は UTC を明示する。

OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);

UseCase 層では Clock をコンストラクタ注入することを基本とする。Clock を注入することでテスト時に任意の時刻を注入でき、テスタビリティが向上する。

// UseCase での Clock 注入例
private final Clock clock;
public CreateFeatureAUseCase(FeatureARepository repository, TransactionManager tx, Clock clock) {
this.repository = repository;
this.transactionManager = tx;
this.clock = clock;
}
OffsetDateTime now = OffsetDateTime.now(clock);

タイムゾーンの扱いは暗黙的な環境依存を避け、明示的に指定する。


ログは SLF4J を使用する。

private static final Logger log = LoggerFactory.getLogger(CreateFeatureAUseCase.class);

Lombok は使用しない(第 26 節参照)。LoggerFactory.getLogger を直接使用する。

セキュリティイベント(認証失敗・権限エラー等)を記録する。

UseCase・Domain には Observability の計装コード(io.micrometerio.opentelemetry)を書かない。


レベル用途
ERRORシステムエラー、処理継続不可
WARN業務上の警告、想定内エラー
INFO処理開始、処理終了、主要な業務イベント
DEBUG開発時の詳細確認

以下はログに出力しない。

  • パスワード
  • アクセストークン
  • リフレッシュトークン
  • API キー
  • 秘密鍵
  • 個人情報
  • 認証ヘッダ
  • Cookie

禁止例:

log.info("Authorization={}", authorizationHeader);
log.info("password={}", password);

本プロジェクトでは Lombok を使用しない。

Java 25 の record を積極的に活用し、ボイラープレートは record の自動生成機能で代替する。

// Lombok 不使用。record を使用する。
record FeatureAId(long value) {
public FeatureAId {
if (value <= 0) throw new InvalidFeatureAIdException();
}
}

ロガーは SLF4J を直接使用する。

// @Slf4j ではなく直接宣言する
private static final Logger log = LoggerFactory.getLogger(CreateFeatureAUseCase.class);

Javadoc は ///(Markdown コメント)を使用する(Java 25 の機能)。

コメントは「なぜそうしているか」を説明するために使用する。

処理を読めば分かる内容をコメントしない。

避ける例:

// id を取得する
long id = request.getId();

推奨例:

// RLS の SET ROLE はトランザクション内で確実に実行する必要があるため、
// @Transactional ではなく TransactionManager ポートで制御する
return transactionManager.execute(() -> {
...
});

TODO コメントは原則として使用しない。

やむを得ず使用する場合は、理由と対応内容を明記する。

// TODO 認証仕様確定後、ログインユーザー ID を設定する
entity.setCreatedBy("system");

未使用 import は禁止する。

ワイルドカード import は禁止する。

禁止:

import java.util.*;

推奨:

import java.util.List;
import java.util.Optional;

フォーマットは Google Java Format(Spotless)に準拠する。

手動フォーマットではなく ./gradlew spotlessApply を使用する。

インデントは半角スペース 2 つ(Google Java Format 準拠)。

1 行が長くなりすぎる場合は適切に改行する(目安:100 文字以内)。


設定ファイルは application.yaml を基本とする(YAML 形式)。

環境ごとの差分はプロファイル別ファイルに分離する。

application.yaml
application-local.yaml
application-dev.yaml
application-prod.yaml

設定例:

spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
mybatis:
mapper-locations: classpath:mapper/**/*.xml
app:
transaction:
isolation: DEFAULT
timeout: 30
logging:
level:
root: INFO
com.<org>.<feature>: DEBUG

以下の情報をソースコードや Git 管理対象ファイルに直接記載しない。

  • DB パスワード
  • API キー
  • アクセストークン
  • 秘密鍵
  • クライアントシークレット

禁止:

spring:
datasource:
password: mypassword

推奨:

spring:
datasource:
password: ${DB_PASSWORD}

依存性注入はコンストラクタインジェクションを使用する。

Domain・UseCase には @Component@Service を付与しない。

config/AppConfig@Bean を書いて明示的に依存グラフを組み立てる。

推奨:

@Configuration
public class AppConfig {
@Bean
public CreateFeatureAUseCase createFeatureAUseCase(
FeatureARepository featureARepository,
TransactionManager transactionManager) {
return new CreateFeatureAUseCase(featureARepository, transactionManager);
}
@Bean
public FeatureARepository featureARepository(FeatureAMapper featureAMapper) {
return new FeatureARepositoryImpl(featureAMapper);
}
}

禁止:

// 禁止: UseCase に @Service を付ける
@Service
public class CreateFeatureAUseCase { ... }
// 禁止: Domain に @Component を付ける
@Component
public class FlightPlan { ... }

Web 層(@RestController@RestControllerAdviceFilter@Configuration)だけは Spring のライフサイクルに乗せるためアノテーションで登録する。

フィールドインジェクション(@Autowired フィールド注入)は原則使用しない。

禁止:

@Autowired
private FeatureARepository featureARepository;

推奨:

// コンストラクタインジェクション
public CreateFeatureAUseCase(
FeatureARepository featureARepository,
TransactionManager transactionManager) {
this.featureARepository = featureARepository;
this.transactionManager = transactionManager;
}

Spring 管理対象のクラスには適切なアノテーションを付与する。

種別アノテーション
Handler@RestController
Config@Configuration
共通例外ハンドラ@RestControllerAdvice
FilterSpring Filter の登録(WebConfig
UseCaseなし(AppConfig@Bean で登録)
Domainなし(Spring 非依存)
Repository 実装なし(AppConfig@Bean で登録)
MyBatis Mapper@Mapper(または @MapperScan で一括登録)

OpenAPI yaml に定義された以下を実装に反映する(スキーマファースト)。

  • path
  • HTTP method
  • requestBody
  • parameters
  • responses
  • schema
  • required
  • maxLength / minLength
  • format
  • pattern
  • example

生成コードは手編集しない。OAS を変更したら ./gradlew openApiSyncToSrc で再生成する。

Handler は生成された XxxApi インターフェースを implements する。


DB create SQL に定義された以下を Repository 実装(MyBatis Mapper XML・DB 行レコード)へ反映する。

  • テーブル名
  • カラム名
  • データ型
  • nullable
  • length
  • primary key
  • foreign key
  • unique 制約
  • index
  • default 値

DB 行レコードの型や制約は DB create SQL を正とする。

ドメインオブジェクトに ORM アノテーション(@Column 等)を付与しない。


API は URI パス方式でバージョニングする。バージョン番号は整数とする。

内部 API と外部 API はパスレベルで分離する。

具体的なパス設計は機能別方針ドキュメントを参照すること。


複数資料間で矛盾がある場合は、以下の優先順位とする。

  1. 業務仕様書
  2. OpenAPI yaml
  3. DB create SQL
  4. アーキテクチャ方針.md
  5. 本コーディング規約.md
  6. Claude Code の一般判断

判断に迷う場合は、実装内コメントまたは生成結果の説明で仮定を明示すること。


テストコードは以下の方針で作成する。

  • 正常系を必ず作成する
  • 主要な異常系を作成する
  • 業務ロジックは UseCase テストで確認する
  • Handler は HTTP ステータスとレスポンス形式を確認する
  • Repository は MyBatis Mapper の BoundSql および実 DB(Testcontainers)でテストする
  • ArchUnit で層依存を自動検査する

テストクラス名は対象クラス名 + Test とする。

対象クラステストクラス
CreateFeatureAUseCaseCreateFeatureAUseCaseTest
FeatureAHandlerFeatureAHandlerTest
FeatureARepositoryImplFeatureARepositoryImplTest
FeatureAMapperFeatureAMapperBoundSqlTest

UseCase テストでは JUnit 6 と Mockito を使用する。

TransactionManager には NoopTransactionManager(テスト用実装)を使用する。

@ExtendWith(MockitoExtension.class)
class CreateFeatureAUseCaseTest {
@Mock
private FeatureARepository featureARepository;
private CreateFeatureAUseCase useCase;
@BeforeEach
void setUp() {
// NoopTransactionManager: トランザクションなしでアクションを実行する
TransactionManager noopTx = new TransactionManager() {
@Override
public <T> T execute(Supplier<T> action) { return action.get(); }
};
useCase = new CreateFeatureAUseCase(featureARepository, noopTx);
}
@Test
void execute_正常な入力_エンティティを作成して返却する() {
var expected = new FeatureA(...);
when(featureARepository.save(any())).thenReturn(expected);
var result = useCase.execute(new CreateFeatureACommand("テストデータ"));
assertThat(result).isEqualTo(expected);
verify(featureARepository).save(any());
}
@Test
void execute_空のタイトル_InvalidFeatureATitleExceptionをスローする() {
assertThatThrownBy(() -> useCase.execute(new CreateFeatureACommand("")))
.isInstanceOf(InvalidFeatureATitleException.class);
}
}

状態遷移・競合制御が必要な UseCase テストでは、findByIdForUpdate をスタブする(findById ではない)。


Handler テストでは MockMvc を使用する。

@WebMvcTest(FeatureAHandler.class)
class FeatureAHandlerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private CreateFeatureAUseCase createFeatureAUseCase;
@Test
void createFeatureA_正常終了する() throws Exception {
var featureA = new FeatureA(...);
when(createFeatureAUseCase.execute(any())).thenReturn(featureA);
mockMvc.perform(post("/api/v1/feature-a/entities")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title": "テストデータ"}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists());
}
}

テストメソッド名は、テストの意図が明確に分かる名前とする。形式・命名スタイルは開発者に任せる。


アサーションには AssertJ を使用することを推奨する。

推奨:

assertThat(featureA.id()).isEqualTo(expectedId);
assertThat(featureA.title()).isEqualTo("テストデータ");

例外検証:

assertThatThrownBy(() -> useCase.execute(invalidCommand))
.isInstanceOf(FeatureANotFoundException.class)
.hasMessageContaining("エンティティが見つかりません");

ArchUnit によるアーキテクチャテストを必ず作成する。

確認対象:

// Domain が Spring・MyBatis・Infrastructure を参照していないこと
// UseCase が HTTP 固有の型を使用していないこと
// @Transactional が UseCase・Domain に使用されていないこと
// UseCase・Domain が io.micrometer / io.opentelemetry を import していないこと
// generated コードが Handler 層からのみ参照されていること

層またはパッケージを追加したらルールも確認する。


Domain Entity に equals / hashCode を実装する場合は注意する。

record を使用する場合は自動生成されるため通常は問題ない。

カスタム実装が必要な場合は以下を確認すること:

  • 双方向関連を持つ Entity への安易な付与は避ける
  • ID 採番前後で同一性が変わる Entity への注意
  • 遅延ロード項目を持つ場合の注意

Domain や DTO に toString を実装する場合は、機密情報を含めない。

// @Override toString で機密情報を除外する
@Override
public String toString() {
return "FeatureA{id=" + id + ", title=" + title + "}";
}

record を使用する場合はデフォルトの toString で機密情報が出力されないよう注意する。


ドメイン型を API レスポンスとして直接返却しない。

禁止:

@Override
public ResponseEntity<FeatureA> getFeatureA(Long id) {
return ResponseEntity.ok(featureARepository.findById(id).orElseThrow());
}

推奨:

@Override
public ResponseEntity<FeatureAResponse> getFeatureA(Long id) {
var featureA = getFeatureAUseCase.execute(id);
return ResponseEntity.ok(FeatureAResponseMapper.from(featureA));
}

API の HTTP ステータスは以下を基本とする。

処理ステータス
取得成功200 OK
登録成功201 Created
更新成功200 OK
削除成功204 No Content
入力エラー(形式・必須)400 Bad Request
認証エラー401 Unauthorized
認可エラー403 Forbidden
対象なし404 Not Found
競合409 Conflict
ビジネスルール違反422 Unprocessable Entity
サーバエラー500 Internal Server Error

HTTP ステータスの設定は Handler 層の責務であり、UseCase・Domain では設定しない。

認証の詳細は AuthenticationSpecification_Common.md を参照すること。