レイヤードアーキテクチャの代償
多くの開発者は、まず「N層(N-Tier)」スタックから学び始めます。コントローラーがサービスを呼び、サービスがリポジトリを呼び、リポジトリがデータベースと通信するという、おなじみのパターンをご存知でしょう。何十年もの間、これはコードを「整理」された状態に保つためのデフォルトの選択肢でした。
しかし、私は長年、あらゆるエンタープライズプロジェクトで一貫したボトルネックに悩まされてきました。例えば、注文に「DiscountCode(割引コード)」というフィールドを1つ追加する必要があるとします。レイヤードプロジェクトでは、エンジニアが「散弾銃手術(Shotgun Surgery)」と呼ぶ作業を強いられます。Orderエンティティ、OrderDTO、OrderRepository、IOrderService、その実装クラス、そして最後にControllerを開くことになります。以前測ってみたことがありますが、たった1つのカラムを追加するためにタブを切り替えるだけで15分もかかりました。
コードはレイヤーごとに疎結合されていますが、機能(フィーチャー)ごとに密結合されています。このミスマッチこそが、開発を遅く感じさせる原因です。ビジネスロジックを書く時間よりも、40層もの深いフォルダ構造をナビゲートする時間の方が長くなってしまいます。バーティカルスライスアーキテクチャ(VSA)は、コードを「技術的な役割」ではなく「何をするか」に基づいてグループ化することで、この問題を解決します。
10分以内でスライスへ移行する
標準的なプロジェクトでは、フォルダは図書目録のようになり、ロジックがソリューション全体に散らばってしまいます。
/Controllers
- ProductController.cs
/Services
- ProductService.cs
/Repositories
- ProductRepository.cs
/Models
- Product.cs
- ProductDto.cs
バーティカルスライスアーキテクチャでは、技術的な役割でグループ化するのをやめます。代わりに、ビジネスの能力(機能)ごとにグループ化します。クイックなリファクタリングを行うと、次のようになります。
/Features
/Products
/GetProduct
- GetProductEndpoint.cs
- GetProductHandler.cs
- GetProductResponse.cs
/CreateProduct
- CreateProductEndpoint.cs
- CreateProductCommand.cs
- CreateProductHandler.cs
- CreateProductValidator.cs
これで、製品を作成するために必要なものがすべて1か所にまとまりました。ビジネスルールが変更された場合、開くのは1つのフォルダだけです。バリデーションロジックがどこに隠されているかを探して、5つの異なるレイヤーを渡り歩く必要はもうありません。
なぜスライスはレイヤーよりも優れているのか
クリーンアーキテクチャはしばしば「念のため」の抽象化に依存します。「将来データベースを入れ替えるかもしれないから」という理由ですべてのサービスにインターフェースを作成します。しかし現実には、5年間一度も起こらなかったデータベース移行のために、インターフェースの保守に100時間以上費やしているチームをいくつも見てきました。一方で、ビジネス要件は毎週のように変わります。
スライス単位で構築することで、各機能をユニークに保つことができます。ある機能が単純なCRUD操作であれば、ハンドラーがORMを使用してデータベースと直接通信しても構いません。別の機能に複雑な価格設定ロジックが含まれる場合は、リッチなドメインモデルを使用します。すべてのAPIエンドポイントに対して「画一的」な型に無理やりはめ込まれることはありません。
Python/FastAPIによる実践的な例
2,000行に膨れ上がった services.py を作る代わりに、その機能専用のファイルを作成します。以下は、ユーザープロファイルを更新するためのスライスです:
# features/users/update_profile.py
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session
from db import get_db, User
router = APIRouter()
class UpdateProfileRequest(BaseModel):
display_name: str
bio: str
@router.put("/users/me")
def handle_update_profile(request: UpdateProfileRequest, db: Session = Depends(get_db)):
# ロジック、バリデーション、永続化処理がここで行われます。
user = db.query(User).filter(User.id == current_user_id).first()
user.display_name = request.display_name
user.bio = request.bio
db.commit()
return {"status": "success"}
スキーマとロジックを1つのファイルにまとめることで、新機能の「最初のコミットまでの時間」を40%近く短縮したチームもあります。12個のタブを行ったり来たりすることなく、変更のコンテキスト全体を頭の中に保持できるからです。特にPython環境では、その柔軟性が開発スピードに直結します。
混乱させずに共有ロジックを扱う
よく受ける最初の質問は、「コードが重複してしまいませんか?」というものです。2つのスライスが共に特定の税率を計算する必要がある場合、コードをコピー&ペーストするのではありません。
解決策は、真に共有されるルールを Domain または Shared フォルダに移動することです。ただし、ここで注意点があります。単に「現時点で似ているから」という理由だけでコードを共有しないでください。月曜日には同一に見えた2つの機能も、金曜日には全く別のものに進化しているかもしれません。VSAでは、変更時にすべてを壊してしまうような脆い「ゴッドオブジェクト」の抽象化よりも、数行の重複の方を好みます。
メディエーターパターンの活用 (C#)
大規模なプロジェクトでは、メディエーターパターン(MediatRライブラリなど)を使用することで、コントローラーを薄く保つことができます。コントローラーは単にメッセージをブロードキャストし、スライス内の特定のハンドラーがそれを受け取ります。
// Features/Orders/CreateOrder/CreateOrderHandler.cs
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly AppDbContext _db;
public CreateOrderHandler(AppDbContext db) => _db = db;
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)
{
var order = new Order(request.CustomerId);
_db.Orders.Add(order);
await _db.SaveChangesAsync(ct);
return order.Id;
}
}
次のスプリントで実践できるヒント
- 小さく始める: モノリス全体をリファクタリングしようとしないでください。
/Featuresフォルダを作成し、次のチケットをバーティカルスライスとして構築してみましょう。 - 境界を強制する: スライス同士が直接呼び出し合うべきではありません。通信が必要な場合は、イベントや共有ドメインモデルを使用してください。
- リポジトリを削除する: スライスにSQLクエリが1つ必要なだけなら、ハンドラーに直接クエリを書いてください。単純な
SELECT文のために3層の抽象化は必要ありません。 - 分割によるリファクタリング:
/Ordersのようなフォルダが混雑してきたら分割しましょう。/Orders/Cancelや/Orders/Refundは、1つの巨大なOrderServiceよりもはるかに管理が容易です。
レイヤーから機能へと切り替えることで、認知負荷が下がります。バリデーションルールの隣にデータベースクエリがあるのは「散らかっている」ように感じるかもしれませんが、生産性の向上は否定できません。あなたはもはやフォルダの案内係ではなく、より速くデリバリーする機能開発者になれるのです。

