1. test 1
  2. test 2
  3. test 3
  4. test 4
  5. test 5
  6. test 6
  7. test 7
  8. test 8
  9. test 9
コース一覧
Spring Boot入門
状態遷移をルールにする

Spring Boot入門

Java で Web API サーバーを作るための Spring Boot の入門コースです。DI コンテナ、REST コントローラ、リクエスト検証、レイヤ分割、Spring Data JPA、例外ハンドリング、テストまでを扱います。毎レッスン、Spring が裏側でやっている判断そのものを pure Java の関数として自分で書き、フレームワークが何を肩代わりしてくれているのかを手触りで理解できる構成にしました。約 12 時間 (1 日 30 分 × 24 日) で 48 レッスンを修了できます。

1
Spring Bootとは何か
01. Spring Bootで何が作れるのか10分
02. サーブレットからフレームワークへ15分
03. ルーティング表という発想15分
04. プロジェクトの構成10分
05. 起動のしくみと自動設定15分
06. パスのマッチング15分
2
DIコンテナ
01. newで書くと何が困るのか15分
02. コンストラクタインジェクション15分
03. インターフェースへの依存15分
04. @Componentと@Bean15分
05. 自作のDIコンテナ20分
06. Beanのスコープとライフサイクル15分
3
RESTコントローラ
01. RESTという設計方針15分
02. @RestControllerの役割15分
03. @GetMappingとパス変数15分
04. クエリパラメータの受け取り15分
05. @PostMappingとリクエストボディ15分
06. ステータスコードの選び方15分
07. ResponseEntityで組み立てる15分
4
リクエストとレスポンスの変換
01. JSONとオブジェクトの往復15分
02. DTOとエンティティを分ける15分
03. recordでDTOを書く15分
04. 入力検証の基本15分
05. 検証エラーをまとめて返す15分
06. アノテーションによる検証15分
5
レイヤ構成とビジネスロジック
01. 3層アーキテクチャ10分
02. @Serviceに置くもの15分
03. 状態遷移をルールにする15分
04. 料金計算のような業務ロジック15分
05. @Transactionalとは何か15分
06. トランザクションの巻き戻し15分
6
データアクセス
01. @Entityとテーブルの対応15分
02. リポジトリという抽象15分
03. メソッド名からクエリを作る20分
04. 検索条件の組み立て15分
05. ページングと並び替え15分
06. N+1問題15分
7
例外処理と横断的関心事
01. 例外をHTTPに変換する15分
02. @ExceptionHandlerと@ControllerAdvice15分
03. エラーレスポンスの形を決める15分
04. フィルタとインターセプタ15分
05. ログとリクエストの追跡15分
06. 設定値の外部化15分
8
テストと仕上げ
01. テストの種類と使い分け15分
02. Serviceの単体テスト15分
03. モックで依存を差し替える15分
04. MockMvcでエンドポイントを試す15分
05. 本番へ出すための次の一歩10分

状態遷移をルールにする

このレッスンでは、注文ステータスのような状態の変化を、あちこちの if ではなく1か所のルールとして表す書き方を身につけます。

散らばった if は必ず食い違います

注文には状態があります。下書き、申請済み、承認済み、出荷済み、キャンセル済み。そして「承認済みでなければ出荷できない」のような決まりがあります。この決まりを、操作ごとのメソッドの先頭に書いていくと、次のようになります。

Java

public void approve(Order order) { if (!"submitted".equals(order.getStatus())) { throw new IllegalStateException(); } order.setStatus("approved"); } public void ship(Order order) { if (!"approved".equals(order.getStatus())) { throw new IllegalStateException(); } order.setStatus("shipped"); } public void cancel(Order order) { if ("shipped".equals(order.getStatus())) { throw new IllegalStateException(); } order.setStatus("canceled"); }

一見すると素直です。問題は、状態が1つ増えたときに起きます。返品受付という状態を足すとしたら、どのメソッドの if を直せばよいでしょうか。答えは「全部を読み直して判断する」であり、読み飛ばした1つが不具合になります。しかも管理画面用の別クラスや、バッチ処理にも同じ if が写されていると、直し漏れは確実に発生します。

さらに厄介なのが、この形では遷移表の全体像がどこにも存在しないことです。仕様を聞かれたとき、メソッドを順に読んで頭の中で組み立てるしかありません。

表として先に決めます

代わりに、まず表を書きます。ここでは次のルールとします。

現在の状態操作遷移後の状態
draftsubmitsubmitted
submittedapproveapproved
submittedrejectdraft
approvedshipshipped
draftcancelcanceled
submittedcancelcanceled
approvedcancelcanceled

この表に無い組み合わせはすべて不許可です。shipped と canceled は終端の状態で、そこからはどの操作も受け付けません。cancel は shipped と canceled 以外のすべての状態から可能、と言い換えることもできます。

表の形にしておくと、確認したいことが目で確かめられます。canceled から何かできてしまわないか、submitted から直接 ship できてしまわないか。コードを読まずに済むのが表の価値です。

ルールを1か所に集めます

表をそのままコードにします。Java なら列挙型と Map の組み合わせが素直です。

Java

public enum OrderStatus { DRAFT, SUBMITTED, APPROVED, SHIPPED, CANCELED; } public final class OrderTransition { private static final Map<String, OrderStatus> RULES = Map.of( "DRAFT/submit", OrderStatus.SUBMITTED, "SUBMITTED/approve", OrderStatus.APPROVED, "SUBMITTED/reject", OrderStatus.DRAFT, "APPROVED/ship", OrderStatus.SHIPPED, "DRAFT/cancel", OrderStatus.CANCELED, "SUBMITTED/cancel", OrderStatus.CANCELED, "APPROVED/cancel", OrderStatus.CANCELED); public static Optional<OrderStatus> next(OrderStatus current, String action) { return Optional.ofNullable(RULES.get(current.name() + "/" + action)); } }

こうすると、状態を1つ増やす作業は「表に行を足す」という1つの操作になります。呼ぶ側は次のように書きます。

Java

@Service public class OrderService { @Transactional public Order apply(Long id, String action) { Order order = orderRepository.findById(id).orElseThrow(); OrderStatus next = OrderTransition.next(order.getStatus(), action) .orElseThrow(() -> new IllegalStateException("invalid transition")); order.setStatus(next); return orderRepository.save(order); } }

呼ぶ側から状態の知識が消えました。Service は「遷移できるか聞いて、できたら適用する」だけになっています。

置き場所の話

この遷移ルールは Controller には置きません。管理画面から呼んでもバッチから呼んでも同じ答えでなければならないからです。Service に置いてもよいですし、状態を持つドメインオブジェクト自身のメソッドとして持たせる形もよく使われます。避けるべきなのは、画面ごとに書き写すことと、データベースの制約だけに頼ることです。

演習で書くもの

演習では、上の表をそのまま実装します。現在の状態と操作を受け取り、遷移できるなら遷移後の状態、できないなら invalid を返します。Spring の Service が引き受ける判断のうち、最も典型的なものの1つです。

要件

  1. 遷移できる場合は遷移後の状態名を、できない場合は invalid という文字列を返すこと
  2. shipped と canceled からはどの操作も受け付けず invalid を返すこと
  3. 未知の状態名や未知の操作名、null が渡された場合も invalid を返すこと

入出力例

transit("draft", "submit") → "submitted" transit("submitted", "approve") → "approved" transit("submitted", "reject") → "draft" transit("approved", "ship") → "shipped" transit("approved", "cancel") → "canceled" transit("shipped", "cancel") → "invalid" transit("draft", "ship") → "invalid" transit("canceled", "submit") → "invalid" transit("unknown", "submit") → "invalid"

ヒント

現在の状態と操作をつないだ current + "/" + action をキーにした Map を1つ作れば、表をそのまま書き写せます

Map の get は見つからないと null を返します。null なら invalid を返す形にすれば、未知の状態も未知の操作も同じ経路で処理できます

null チェックはメソッドの先頭で済ませてしまうと、後の処理を素直に書けます

生田 陸人
監修生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア監修者プロフィールを見る →
編集 ゆめさく編集部·公開 2026/08/09

関連レッスン

  • 料金計算のような業務ロジック

    条件が絡む計算を、読める形で実装します。

  • @Transactionalとは何か

    トランザクション境界とロールバックの条件を確認します。

  • トランザクションの巻き戻し

    失敗時に変更をなかったことにする処理を実装します。

  • @Entityとテーブルの対応

    エンティティのマッピングと主キーの指定を確認します。

このレッスンに出てくる用語

意味があいまいなまま進んだ語は、ここから読み直せます。

  • メソッドクラスに属する関数
  • 判断YES/NO 分岐を表す菱形
  • クラスデータと振る舞いを束ねる単位
  • 処理計算や代入を表す長方形
  • ドメインサイト名(example.com)
  • オブジェクトキーと値のペアで構造を作るデータ
  • データベースデータを構造化して保存する仕組み
main.java
学習モード
エディタを読み込んでいます

メモ

状態遷移をルールにする

⌘S で保存