AIによる要約
注文取消では、対象を取り出し、取り消せる状態かを判断し、成立した場合だけ保存します。この順番をServiceのユースケースにまとめると、HTTPを介さず同じ取消と保存の契約を確認できます。


注文100は出荷待ち、101は出荷済み、102は取消済みです。この機能では出荷待ちだけを取り消します。状態を変えられない注文と、存在しない注文も、それぞれ結果を決めます。
ここでは、HTTPコードを使わずに「どの状態なら変わるか」「失敗した注文は保存されないか」を調べます。
この記事のポイント
- 出荷待ちの取消だけを保存する
- Controllerと別の入口が同じServiceを使う
- Repositoryは取得と保存に集中する
- 業務ルールをHTTPなしのJUnitで確かめる
実行用サンプル
OrderServiceを直接呼び、正常・出荷済み・取消済み・未登録の4件で、結果と保存回数を確かめます。同じServiceをHTTPから呼ぶMockMvcの6件も収録しています。
JDK 17、Maven 3.9系、Spring Boot 3.5.16。ZIPにはソース・JUnit・pom.xml・READMEが入っています。展開先で mvn test を実行します。
Serviceは業務処理の置き場所
取消ユースケースの順は、注文を取得し、現在の状態で取消できるか確かめ、成立した注文を保存する、です。Order.canCancel() は出荷待ちだけを許可し、OrderService.cancel() はその規則を呼んでから保存します。
OrderService.java の全文です。
package example.orders;
import org.springframework.stereotype.Service;
@Service
public final class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public OrderResult detail(long id) {
return OrderResult.from(requireOrder(id));
}
public OrderResult cancel(long id) {
Order order = requireOrder(id);
if (!order.canCancel()) {
throw new CannotCancelOrderException();
}
Order canceled = order.canceled();
repository.save(canceled);
return OrderResult.from(canceled);
}
private Order requireOrder(long id) {
return repository.findById(id).orElseThrow(OrderNotFoundException::new);
}
public record OrderResult(long id, Order.Status status, boolean cancelable) {
private static OrderResult from(Order order) {
return new OrderResult(order.id(), order.status(), order.canCancel());
}
}
public static final class OrderNotFoundException extends RuntimeException {}
public static final class CannotCancelOrderException extends RuntimeException {}
}OrderResult は呼び出し側へ返す結果です。取得や保存に使う注文の全項目と、HTTPのステータスは渡しません。
Controllerに書くと入口ごとに散らばる
既存のAPIに加えてバッチで取消を行うとき、Controllerにある条件をコピーすると、片方だけ「取消済みも処理する」変更が入ることがあります。入口が増えても cancel(id) を使えば、出荷済みと取消済みを保存しない判断を一箇所で変更できます。
HTTP側は業務例外を404や409へ変換します。バッチ側なら同じ業務例外を処理結果へ記録できます。どちらもServiceの処理にHTTPの有無を渡す必要はありません。
Serviceに書くとテストしやすい
出荷済みのケースは、例外だけでなく元の状態が残り、保存が0回であることを確かめます。誤って例外の前に保存した場合、このテストは失敗します。
OrderServiceTest.java の shippedOrderIsRejectedWithoutSaving() の抜粋です。
@Test
void shippedOrderIsRejectedWithoutSaving() {
repository.reset(new Order(101, SHIPPED, "REF-101"));
assertThrows(CannotCancelOrderException.class, () -> service.cancel(101));
assertEquals(SHIPPED, repository.stored(101).status());
assertEquals(0, repository.saves);
}| 対象 | 期待結果 | 保存回数 |
|---|---|---|
| 100 出荷待ち | CANCELEDへ更新 | 1 |
| 101 出荷済み | CannotCancelOrderException | 0 |
| 102 取消済み | CannotCancelOrderException | 0 |
| 999 未登録 | OrderNotFoundException | 0 |
RepositoryはDBアクセスに集中する
OrderRepository の契約はIDで見つけることと、受け取った注文を保存することです。テストはメモリ実装へ差し替えています。
OrderRepository.java の全文です。
package example.orders;
import java.util.Optional;
public interface OrderRepository {
Optional<Order> findById(long id);
void save(Order order);
}実システムで取消と在庫の復元を同時に行うなら、どの更新を一緒に確定・取消するかを設計します。このサンプルは保存の呼び出しまでで、DBのトランザクションや競合制御は扱いません。
Serviceは何でも入れる場所ではない
取消可否は注文の状態に関わるので Order に置き、取得と保存の順をServiceに残しました。通知や料金計算が独立して変わる規模になったら、単に行数で切る前に、どの変更がどこへ影響するかを確認します。
DIとServiceの関係
実行時はSpringがコンストラクタにRepositoryを注入します。単体テストでは同じコンストラクタへメモリ実装を渡すので、Springの起動やHTTPを使わず取消規則を確認できます。MockMvc側ではテスト用のBootコンテキストにControllerとServiceを登録しています。
現場レビューではどう見られるか
「出荷済みで例外が出ることに加え、保存が0回で状態も残ることをテストしてください。管理画面で同じ取消を追加しても、この規則はServiceで一度だけ直せるようにします。」
レビュー前のセルフチェック
- 出荷待ちだけがCANCELEDに変わり、保存が1回か。
- 出荷済み・取消済み・未登録は保存が0回か。
- Controller以外の入口からも同じServiceメソッドを使えるか。
- Serviceの結果や例外にHTTPステータスを持ち込んでいないか。
この記事とあわせて読みたい
まとめ
Serviceに残すのは、注文を取り出し、状態の規則を適用し、成立した場合だけ保存する順番です。HTTPを通さない4件で、結果と保存の両方を確認できます。



