---
title: "サービスクラスとは？Java・Springでの役割と書き方、コントローラーとの違い"
url: "https://www.issoh.co.jp/tech/details/3015/"
published: 2024-07-10
updated: 2026-10-19
categories: ["Spring"]
publisher: "株式会社一創"
---

# サービスクラスとは？Java・Springでの役割と書き方、コントローラーとの違い

サービスクラスは、アプリケーションが提供する処理（注文を確定する、会員を登録するなど）の手順と、その処理を1つのトランザクションにまとめる範囲を受け持つクラスです。JavaのSpringでは `@Service` を付けたクラスがこれに当たり、コントローラーから呼ばれ、リポジトリを使ってデータを読み書きします。

ただし「サービスクラス」が指すものは設計の流儀で違います。この記事では3つの意味を切り分けたうえで、Spring Boot 4.1での書き方、状態を持たせない理由、`@Transactional` の落とし穴を、H2データベースで実行したコードで説明します。

## まとめ：サービスクラスの役割と書き方の要点

- **役割**：1つの処理の手順を組み立て、トランザクションの範囲を決める層。HTTPの受け取りはコントローラー、DBの読み書きはリポジトリに任せます。
- **3つの意味**：トランザクションスクリプトの「サービス」、DDDの「アプリケーションサービス」（ユースケースと呼ぶ流儀もある）、DDDの「ドメインサービス」。議論がかみ合わないときは、どれの話かを先にそろえます。
- **Springの `@Service`**：`@Component` の特殊化で、コンポーネントスキャンでBeanとして登録されるだけです。`@Service` を付けたこと自体で増える機能はありません。
- **書き方**：依存はコンストラクタで受け取り、更新系のpublicメソッドに `@Transactional` を付けます。コンストラクタが1つなら `@Autowired` は不要です。
- **状態を持たせない**：`@Service` のBeanは既定でシングルトンなので、フィールドに入れた値は全リクエストで共有されます。リクエストごとの値は引数とローカル変数で扱います。
- **落とし穴**：同じクラス内から呼んだメソッドの `@Transactional` は効かず、チェック例外では既定でロールバックされません。どちらも実DBで「消えるはずの行が残る」形で現れます。

## サービスクラスの定義と3つの意味

「サービス」という名前のクラスは、少なくとも次の3種類の意味で使われています。同じ「サービスクラスは必要か」という問いでも、どれを指すかで答えが変わります。

| 呼び名                      | 出どころ                             | 持つもの         | 例                       |
| ------------------------ | -------------------------------- | ------------ | ----------------------- |
| Transaction Script型のサービス | Fowler『PoEAA』のTransaction Script | 処理の手順とルールの両方 | OrderService.placeOrder |
| アプリケーションサービス             | DDD（Evans）                       | 処理の手順のみ      | PlaceOrderUseCase       |
| ドメインサービス                 | DDD（Evans）                       | 特定の業務ルールのみ   | TransferService（口座間の振替） |

### トランザクションスクリプト型サービスの責務：手順と業務ルール

Martin Fowlerの『Patterns of Enterprise Application Architecture』（2002年）に収録された[Service Layer](https://martinfowler.com/eaaCatalog/serviceLayer.html)（Randy Stafford執筆）は、この層を「アプリケーションの境界を、利用可能な操作をそろえたサービスの層で定義し、各操作でのアプリケーションの応答を調整するもの」と説明しています。サービス層の実装方法の一つが同書のTransaction Script（プレゼンテーションからの1リクエストを1つの手続きで処理するパターン）で、この形では1メソッドが1つの処理の手順と、金額チェックなどのルールをまとめて持ち、エンティティはデータの入れ物に近くなります。

### アプリケーションサービスの責務とユースケースとの関係

[ドメイン駆動設計（DDD）とは？意味・基本用語・実装と採用判断をわかりやすく解説](/tech/details/3580/)で扱うDDDでは、業務ルールはエンティティや値オブジェクトに置き、サービスは「リポジトリから取り出す→ドメインオブジェクトに処理を頼む→保存する」という手順だけを持ちます。クリーンアーキテクチャの流儀ではこれを「ユースケース（Interactor）」と呼ぶことが多く、[Robert C. Martinの原典](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)ではユースケースもアプリケーション固有の業務ルールを持ちます。そのため「usecaseとserviceの違い」は名前の違いではなく、手順とアプリケーション固有の判断だけを持つアプリケーションサービス（ユースケース）か、エンティティの業務ルールまで抱えるトランザクションスクリプト型のサービスか、の違いとして見ます。

名前は流儀で揺れ、1クラス1処理の `PlaceOrderUseCase`、CQRSに寄せた `PlaceOrderCommandHandler` なども中身はアプリケーションサービスです。呼び名を混ぜると「どこにルールを書くか」の約束が読めなくなるので、プロジェクトで1つにそろえます。

### ドメインサービス：エンティティに属さない業務ルールの置き場

Evansの『Domain-Driven Design』（2003年）がSERVICEパターンの中心に置いたのはこちらで、特定のエンティティに属させると不自然な業務ルール（2つの口座をまたぐ振替など）を、状態を持たない操作として切り出したものです。手順を組み立てるアプリケーションサービスとは層が違い、トランザクションの管理は持たせず、DBアクセスも原則としてアプリケーションサービス側に任せます。

## Springの@Serviceの役割とBean登録の仕組み

Spring Frameworkの[@Service のJavadoc](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Service.html)は、このアノテーションを付けたクラスを、Evans（2003年）の定義による「モデルの中で独立し、カプセル化された状態を持たない、インタフェースとして提供される操作」、または「Business Service Facade」（Core J2EEパターンの意味）を表すものと説明し、チームごとに意味を絞って使ってよい汎用のステレオタイプだとしています。つまりSpringは、上の3つのどの意味で使うかを決めていません。

動作の面では、`@Service` は `@Component` の特殊化で、クラスパススキャンで実装クラスを自動検出させるためのものです。標準のBean登録では `@Component` に付け替えても同じように動きます。ただし、`@Service` を対象とする独自のAOPやスキャン条件がある場合は挙動が変わります。`@Repository` だけは、`PersistenceExceptionTranslationPostProcessor` によるデータアクセス例外の変換対象になるという違いがあります。各アノテーションの一覧は[Spring Bootのアノテーション一覧｜役割別の早見表とBoot 4での変更点](/tech/details/4048/)にまとめています。

| アノテーション                       | 付ける層       | @Component以外の働き  |
| ----------------------------- | ---------- | ---------------- |
| @Controller / @RestController | プレゼンテーション層 | リクエストマッピングの対象になる |
| @Service                      | サービス層      | なし（役割の目印）        |
| @Repository                   | データアクセス層   | 永続化例外の変換対象になる    |

BeanがどのようにDIコンテナへ登録されるかは[IoCコンテナとは？制御の反転の仕組みとSpring IoCコンテナ・Beanをわかりやすく解説](/tech/details/3210/)を参照してください。

## Controller・Service・Repositoryの責務分担

3層の役割は「何を知っているか」で分けると判断しやすくなります。コントローラーはHTTPを知っていてDBを知らない、リポジトリはDBを知っていて業務の手順を知らない、サービスはその間で手順を組み立てる、という分担です。画面や表示との関係を含めた構造は[MVCとは？MVVM・MVPとの違いを同じ機能のコードで比較](/tech/details/7040/)で説明しています。

| 層          | 書くもの                | 書かないもの         |
| ---------- | ------------------- | -------------- |
| Controller | URL・入力の受け取り・レスポンス形式 | 業務ルール・トランザクション |
| Service    | 処理の手順・トランザクション範囲    | HTTPの型・SQL     |
| Repository | 保存・検索のクエリ           | 業務ルール・画面の都合    |

### コントローラーと業務ルールの責務境界

コントローラーの中で金額チェックや在庫の引き当てを書くと、同じ処理をバッチやメッセージ受信から呼びたいときに、HTTP用の型やコントローラーの責務に引きずられ、再利用しにくくなります。コントローラーのメソッドは、リクエストをサービスの引数に詰め替えて1回呼び、結果を返すだけの数行に収まるのが目安です。逆に、`HttpServletRequest` のようなHTTP固有の型や、HTTPの仕様に結び付いたDTOをサービスの引数・戻り値に使うと、サービスがHTTPに依存してしまいます。HTTPに依存しないユースケース用の入出力DTOは利用できます。

### サービス層とリポジトリ層の境界

リポジトリは、Spring Data JPAのJpaRepositoryのように保存・検索・一括更新などの永続化操作を担い、サービスは「保存し、決済し、支払済みにする」という複数の操作の順序と、それを1つのトランザクションにまとめる範囲を担います。SQLはリポジトリにだけ置きます。

## Spring Boot 4.1でのサービスクラスの書き方

ここでは注文を受け付ける処理を例にします。コードは2026年10月時点の最新安定版であるSpring Boot 4.1.1（Spring Framework 7.0.9・Hibernate 7.4.5）とJDK 25で、`spring-boot-starter-webmvc`・`spring-boot-starter-data-jpa`・H2を入れたプロジェクトを作り、テストまで実行して確認しました。プロジェクトの作り方は[Spring Bootとは｜4.1系の依存名の変更と最小Web APIの作り方](/tech/details/17478/)を参照してください。

### エンティティとJpaRepositoryの最小構成

エンティティは状態と、その状態を変える操作を持ちます。「支払済みの注文はキャンセルできない」というルールも、注文の状態だけで判定できるのでエンティティの `cancel` に置きます。リポジトリはSpring Data JPAの `JpaRepository` を継承したインタフェースで、実装クラスは書きません。

```
package com.example.order;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue
    private Long id;
    private Long customerId;
    private int amount;
    private String status;

    protected Order() {}

    public Order(Long customerId, int amount) {
        this.customerId = customerId;
        this.amount = amount;
        this.status = "NEW";
    }

    public void markPaid() { this.status = "PAID"; }

    // 業務ルールはエンティティが持つ
    public void cancel() {
        if ("PAID".equals(status)) {
            throw new IllegalStateException("paid order cannot be cancelled");
        }
        this.status = "CANCELLED";
    }

    public Long getId() { return id; }
    public Long getCustomerId() { return customerId; }
    public int getAmount() { return amount; }
    public String getStatus() { return status; }
}
```

```
package com.example.order;

import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
}
```

### コンストラクタインジェクションで書くサービスクラス本体

依存するリポジトリと決済の窓口（`void charge(Long customerId, int amount)` だけを持つ `PaymentGateway` インタフェース）は、コンストラクタで受け取って `final` フィールドに入れます。[Springの公式リファレンス](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html)は、コンポーネントを不変にでき、必須の依存がnullにならないことを理由に、コンストラクタインジェクションを推奨しています。コンストラクタが1つだけのクラスでは、`@Autowired` を付けなくてもそのコンストラクタが使われます。Springから起動するには `PaymentGateway` の実装もBeanとして登録する必要があり、今回の結合テストでは何も処理しない実装をテスト用の `@Bean` で登録しました。注入方式の違いは[SpringのDI（依存性注入）とは？仕組みと注入方法を動くコードでわかりやすく解説](/tech/details/2303/)で詳しく扱っています。

```
package com.example.order;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;

    // コンストラクタが1つだけなので @Autowired は不要
    public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
        this.orderRepository = orderRepository;
        this.paymentGateway = paymentGateway;
    }

    @Transactional
    public Order placeOrder(Long customerId, int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
        Order order = orderRepository.save(new Order(customerId, amount));
        paymentGateway.charge(customerId, amount);
        order.markPaid();
        return order;
    }

    @Transactional
    public void cancelOrder(Long id) {
        Order order = orderRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("order not found: " + id));
        order.cancel();   // save() を呼ばなくても、コミット時に変更が反映される
    }

    @Transactional(readOnly = true)
    public Order findOrder(Long id) {
        return orderRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("order not found: " + id));
    }
}
```

`placeOrder` は「保存→決済呼び出し→支払済み」の順に処理し、決済で `RuntimeException` が投げられれば保存した注文もロールバックされます。ただし取り消せるのはDBの変更だけで、外部決済APIで済んだ課金までは戻りません。決済成功後にコミットが失敗する場合に備え、実際の決済連携には冪等性や取消・照合の設計が必要です。`cancelOrder` はJPAの変更検知を示すため、取り出したエンティティに処理を頼み、`save` を省略しています。トランザクション内で読み込んだエンティティはJPAの管理下にあり、変更はコミット時にDBへ書き込まれます（H2で実行し、キャンセル後の状態が `CANCELLED` になることを確認しました）。ただし[Spring Data JPAの公式リファレンス](https://docs.spring.io/spring-data/jpa/reference/jpa/transactions.html)は、JPAとしては不要でも、リポジトリ抽象との一貫性のために `save` を呼ぶことを勧めています。参照だけの `findOrder` には `readOnly = true` を付け、更新しない意図を明示しています。

### コントローラーからの呼び出し方

コントローラーはサービスをコンストラクタで受け取り、リクエストの値を渡して結果を返すだけにします。

```
package com.example.order;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    public record PlaceOrderRequest(Long customerId, int amount) {}

    @PostMapping
    public Order place(@RequestBody PlaceOrderRequest request) {
        return orderService.placeOrder(request.customerId(), request.amount());
    }

    @GetMapping("/{id}")
    public Order find(@PathVariable Long id) {
        return orderService.findOrder(id);
    }
}
```

サンプルでは短くするためエンティティをそのまま返していますが、実務ではレスポンス用のrecordに詰め替え、JPAの遅延読み込みやフィールドの露出をHTTPに持ち込まないようにします。

## サービスクラスに状態を持たせない理由：シングルトンとフィールドの共有

Springの[Beanスコープの公式リファレンス](https://docs.spring.io/spring-framework/reference/core/beans/factory-scopes.html)は、シングルトンを既定のスコープとし、「状態を持つBeanにはprototype、状態を持たないBeanにはsingletonを使う」のを原則としています。`@Service` を付けたクラスは何も指定しなければシングルトンで、1つのSpringコンテナ内の1つのBean定義につきインスタンスは1つです。同時に届いたリクエストは、同じインスタンスのメソッドを別々のスレッドで実行します。

そのため、フィールドに「今処理している顧客ID」や「計算途中の合計」を入れると、別のリクエストの値で上書きされます。次の `CounterService` はその最小例です。

```
@Service
public class CounterService {
    private int count;          // シングルトンのフィールド＝全スレッドで共有される

    public void increment() { count++; }
    public int getCount() { return count; }
}
```

Spring Boot上で `getBean(CounterService.class)` を2回呼ぶと同じインスタンスが返ります（`==` の比較が `true`）。この共有を再現するため、1つの `CounterService` インスタンスを2スレッドで共有して各10万回 `increment()` を呼ばせ、両スレッドの終了を待って件数を表示しました。5回の結果は次のとおりで、一度も200,000になりませんでした（結果は実行ごとに変わり、200,000になる回もあります）。

```
2スレッド x 100000回 -> count = 133766
2スレッド x 100000回 -> count = 143998
2スレッド x 100000回 -> count = 198908
2スレッド x 100000回 -> count = 198454
2スレッド x 100000回 -> count = 198999
```

`count++` は「読む・足す・書く」の3段階で、2つのスレッドが同じ値を読んで書き戻すと1回分が消えます。フィールドには依存オブジェクトや不変の設定値を置き、リクエストごとの値は引数とローカル変数で渡すのを原則にします。どうしても共有する可変の値（集計値など）は、`AtomicInteger` のような原子的な更新を使うかDBに持たせます。

## @Transactionalの落とし穴：自己呼び出しとチェック例外

Springの宣言的トランザクションは、既定のプロキシモードではBeanをプロキシで包み、外から呼ばれたときだけトランザクションを開始します。実際、上の `OrderService` をコンテナから取り出してクラス名を表示すると `com.example.order.OrderService$$SpringCGLIB$$0` で、注入されているのは元のクラスを継承したプロキシです。仕組みそのものは[AOP（アスペクト指向プログラミング）とは？Spring AOPの仕組みと使い方・自己呼び出しの落とし穴](/tech/details/3422/)で解説しています。

この仕組みから起きる2つの挙動を、次のサービスで確かめました。どのメソッドも注文を1件保存してから例外を投げます。トランザクションが効いてロールバックされれば、テーブルの行数は0のはずです。

```
package com.example.order;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PointService {

    private final OrderRepository orderRepository;

    public PointService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void outer() {
        inner();   // this.inner() なのでプロキシを通らない
    }

    @Transactional
    public void inner() {
        orderRepository.save(new Order(1L, 100));
        throw new RuntimeException("fail after save");
    }

    @Transactional
    public void checked() throws Exception {
        orderRepository.save(new Order(1L, 100));
        throw new Exception("checked");
    }

    @Transactional(rollbackFor = Exception.class)
    public void checkedWithRollbackFor() throws Exception {
        orderRepository.save(new Order(1L, 100));
        throw new Exception("checked");
    }
}
```

テスト側には `@Transactional` を付けず、各ケースの実行前に `orderRepository.deleteAll()` で注文を削除します。そのうえで `@SpringBootTest` から各メソッドを呼び、例外を捕捉した直後に `orderRepository.count()` を表示した結果です。

```
inner() を外から呼ぶ -> rows = 0
outer() 経由で inner() -> rows = 1
チェック例外 -> rows = 1
チェック例外 + rollbackFor -> rows = 0
```

### 自己呼び出しで無効になるトランザクション

外から `inner()` を呼ぶと保存がロールバックされて0件、`outer()` 経由だと例外が出たのに1件残りました。[Springの公式リファレンス](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html)は、プロキシモード（既定）では外部からの呼び出しだけがインターセプトされ、同じオブジェクト内の別メソッドを呼ぶ「自己呼び出し」では、呼ばれたメソッドに `@Transactional` があってもトランザクションは始まらないと明記しています。1件残ったのは、`inner()` を包むトランザクションが無く、Spring Data JPAの `save` が自前のトランザクションでコミットしたためです。

対策は、外から呼ばれる入口のメソッドに `@Transactional` を付けるか、別のBeanに切り出して注入することです。なお、Spring 6.0以降、クラスベースのプロキシでは `protected` やパッケージ可視のメソッドも対象になりましたが、インタフェースベースのプロキシでは今も `public` でインタフェースに定義したメソッドに限られます。

### チェック例外でコミットされる既定のロールバック規則

[ロールバック規則の公式リファレンス](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html)のとおり、既定でロールバックされるのは `RuntimeException` とそのサブクラス、および `Error` だけで、チェック例外（`Exception` を直接継承するものなど）が投げられたときはコミットされます。実測でも、チェック例外を投げた `checked()` は1件残り、`rollbackFor = Exception.class` を付けた方は0件でした。

業務エラーをチェック例外で表しているコードベースでは、`@Transactional(rollbackFor = Exception.class)` を付けるか、非チェック例外に変えます。ロールバックの仕組みやACIDとの関係は[トランザクションとは？分離レベルの違い・ACID・commit/rollbackを実装目線で解説【2026年版】](/tech/details/13690/)を参照してください。

## サービスクラスの単体テスト（JUnitとMockito）

コンストラクタインジェクションで書いたサービスは、Springを起動せずに `new` でテストできます。リポジトリと決済窓口をMockitoのモックに差し替え、「保存して決済し支払済みにする」ことと「金額が不正なら何もしない」ことを確かめます。

```
package com.example.order;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;
import org.mockito.InOrder;

class OrderServiceTest {

    OrderRepository repository = mock(OrderRepository.class);
    PaymentGateway gateway = mock(PaymentGateway.class);
    OrderService service = new OrderService(repository, gateway);

    @Test
    void 注文を保存して決済し支払済みにする() {
        when(repository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0));

        Order order = service.placeOrder(1L, 3000);

        assertEquals("PAID", order.getStatus());
        InOrder calls = inOrder(repository, gateway);   // 保存してから決済したか
        calls.verify(repository).save(order);
        calls.verify(gateway).charge(1L, 3000);
    }

    @Test
    void 金額が0以下なら保存も決済もしない() {
        assertThrows(IllegalArgumentException.class, () -> service.placeOrder(1L, 0));

        verify(repository, never()).save(any());
        verify(gateway, never()).charge(any(), anyInt());
    }
}
```

Spring Boot 4.1.1が管理するJUnit 6.0.3とMockito 5.23.0で実行し、2件とも成功しました。JDK 25では、自己アタッチは将来のJDKで動かなくなるという警告が出るので、[Mockitoの公式ドキュメント](https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3)に沿ってテストJVMに `-javaagent` でMockitoのJARを指定します。このテストはトランザクションを通さないため、ロールバックの確認には前章のように `@SpringBootTest` などSpringを起動するテストを別に用意します。キャンセル可否のようにエンティティに置いたルールは、モックも要らず `new Order(1L, 3000)` に `markPaid()` と `cancel()` を呼ぶだけでテストできます。モックとスタブの使い分けは[モックとは？スタブとの違い・テストダブル5分類と5言語の書き方を実行例で解説](/tech/details/13505/)で整理しています。

## サービスクラスを作らないほうがよい場面とFat Serviceの見分け方

サービスクラスは「とりあえずControllerとRepositoryの間に置く層」として作られがちで、その結果、業務ルールがすべてサービスに集まり、エンティティがgetterとsetterだけになります。Fowlerはこれを[Anemic Domain Model（ドメインモデル貧血症）](https://martinfowler.com/bliki/AnemicDomainModel.html)と呼び、振る舞いを全部サービスへ移すと実質的にTransaction Scriptになると書いています。次のどれかに当てはまるなら、サービスに書く前に置き場所を見直します。

- **1つのエンティティの状態だけで判定できるルール**（注文がキャンセル可能か、など）：エンティティのメソッドにします。書き換え方は次の節で示します。
- **変更理由の異なる処理が1つのサービスに同居する**（`OrderService` に集計・CSV出力・メール送信・検索が同居する）：処理ごとにクラスを分けます。分け方の基準は[SRP（単一責任の原則）とは｜具体例とSRP違反・やりすぎ（過剰分割）の見極めをわかりやすく解説](/tech/details/4523/)が参考になります。
- **コンストラクタの引数が増え続ける**：Springの公式リファレンスも、コンストラクタ引数の多さはクラスの責務が多すぎる兆候だと述べています。
- **リポジトリを1回呼んで返すだけのメソッドしかない**：CRUDだけの画面なら、サービスを挟まずコントローラーからリポジトリを呼ぶ構成で足ります。ただしトランザクションを複数操作にまたがせる必要が出た時点でサービスを置きます。

### 業務ルールをエンティティへ戻す書き換え例

Fat Serviceでよく見るのは、サービスがエンティティの状態を取り出して判定し、setterで書き戻す形です。

```
// 書き換え前：ルールがサービスにあり、Orderは setStatus を持つ状態の入れ物
@Transactional
public void cancelOrder(Long id) {
    Order order = orderRepository.findById(id).orElseThrow();
    if (order.getStatus().equals("PAID")) {
        throw new IllegalStateException("paid order cannot be cancelled");
    }
    order.setStatus("CANCELLED");
    orderRepository.save(order);
}
```

バッチや管理画面がこのサービスを使わず、注文の状態を直接変更する実装を増やすと、同じ `if` を各所に書き写すことになり、1か所でも漏れると支払済みの注文がキャンセルされます。判定を `Order.cancel()` に移せば `setStatus` を公開せずに済み、ルールを通らずに状態を変える道が消えます。書き換え後のサービスは、前章の `cancelOrder` のとおり「取り出して `order.cancel()` を呼ぶ」だけです。サービスに残すのは、複数のエンティティや外部APIをまたぐ手順とトランザクションの範囲です。

### Railsのapp/servicesが議論になる理由

「サービスクラス」で検索するとRailsの議論が上位に出ます。[Railsの公式ガイド](https://guides.rubyonrails.org/autoloading%5Fand%5Freloading%5Fconstants.html)は、`app/services` はデフォルトでは生成されないが、存在すれば自動読み込みの対象になると説明しています。Railsはモデルに業務ルールを持たせる設計が前提なので、サービスクラスを置くとモデルとの二重構造になりやすい、というのが批判の中心です。Springでもサービス層の採否は、フレームワーク名ではなく責務の重複と再利用の必要性で決まり、ルールをエンティティに置くという主張は上の書き換え例のとおりJavaでも通用します。

層の分け方やトランザクション設計を含めて業務システムを作る場面では、弊社の[基幹システム開発](/service/business%5Fsystem/core%5Fbusiness/)でも設計段階から相談を受けています。

## よくある質問

### @Serviceの代わりに@Componentを付けても動きますか？

標準の設定なら動きます。`@Service` は `@Component` の特殊化で、Beanとして登録される点は同じです。違いは読み手に「サービス層のクラス」だと伝わることなので、層の役割に合わせて `@Service` を使います。

### サービスクラスにインタフェース（ServiceImpl）は必要ですか？

実装が1つしかないなら、必須ではありません。Springはインタフェースが無くてもクラスベースのプロキシで `@Transactional` を適用でき（上の `OrderService$$SpringCGLIB$$0` がそれです）、Mockitoもクラスをそのままモックにできます。支払い方法ごとに実装を切り替えるなど、実装が複数ある場合にインタフェースを切ります。

### サービスクラスから別のサービスクラスを呼んでもよいですか？

呼んでかまいませんが、循環させないことが条件です。AがBを、BがAを依存として受け取る構成は、コンストラクタインジェクションでは起動時にエラーになります。Spring Bootでは `spring.main.allow-circular-references` の既定が `false` のため、フィールドインジェクションでも同様に起動できません。共通部分を第3のクラスに切り出すか、呼び出しを一方向にそろえます。

### サービスクラスとユーティリティクラス（Util）の違いは何ですか？

ユーティリティクラスは日付の整形や文字列変換のように、状態も依存も持たない `static` メソッドの集まりです。サービスクラスはリポジトリなどの依存を注入され、業務の処理の手順を持ちます。業務の手順を組み立てるならサービス、日付整形など業務固有でない共通処理ならユーティリティが候補です。入力だけで結果が決まる計算でも、料金計算などの業務ルールならエンティティ・値オブジェクト・ドメインサービスに置きます。

### Service層とは何ですか？

コントローラー（プレゼンテーション層）とリポジトリ（データアクセス層）の間に置く、サービスクラスの集まりです。処理の手順とトランザクションの範囲を受け持ち、バッチやメッセージ受信などHTTP以外の入口からも同じ処理を呼べることが、層を分ける一番の利点です。

## 関連記事

- [SpringのDI（依存性注入）とは？仕組みと注入方法を動くコードでわかりやすく解説](/tech/details/2303/)
- [Springアノテーションとは？仕組みと主要アノテーションの使い分けを解説](/tech/details/4048/)
- [ドメイン駆動設計（DDD）とは？意味・基本用語・実装と採用判断をわかりやすく解説](/tech/details/3580/)
- [AOP（アスペクト指向プログラミング）とは？Spring AOPの仕組みと使い方・自己呼び出しの落とし穴](/tech/details/3422/)
- [トランザクションとは？分離レベルの違い・ACID・commit/rollbackを実装目線で解説【2026年版】](/tech/details/13690/)

---

出典: [サービスクラスとは？Java・Springでの役割と書き方、コントローラーとの違い](<https://www.issoh.co.jp/tech/details/3015/>)（株式会社一創）
