Vertical Slice Architecture(VSA)は、コントローラ・サービス・リポジトリという技術的な役割ではなく、「注文を取り消す」「請求書を発行する」といった機能単位でコードをまとめる設計手法です。ただし「バーティカルスライス」という言葉は分野によって指すものが違います。ゲーム開発では出資判断に使う遊べるデモ、アジャイルではユーザーストーリーの切り方、書籍『脳に収まるコードの書き方』では最初に作る1本の動く機能を指します。この記事では最初に4つの意味を切り分け、そのうえで設計手法としてのVSAの定義、C#とGoの実装コード、スライス同士の依存をテストで禁止する方法、採用を見送る条件までを扱います。
まとめ:Vertical Slice Architectureの要点
- VSAは、1つの機能に関わる入力・業務ルール・データアクセスを1か所へ束ね、機能間の依存を切る構造です。提唱者Jimmy Bogardの原則は「スライス間の結合を最小化し、スライス内の結合を最大化する」。
- 「バーティカルスライス」はゲーム開発・アジャイル・書籍・設計手法で意味が違います。この記事で扱うVSAは、機能ごとに関心事をまとめ、変更に伴う結合を制御する設計手法です。
- レイヤードやクリーンアーキテクチャが技術的な層でコードを分けるのに対し、VSAは機能を軸にまとめて変更範囲の局所化を図ります。その代わりにスライス間の重複コードを条件付きで許容します。
- スライス同士を参照させない約束は、レビューではなくテストで守らせます。.NETならArchUnitNETの1ルール、Goなら標準ライブラリだけのテスト1本で書けます。ただしどちらも検出範囲に癖があり、何を禁じているかを把握して使います。
- .NETで定番だったMediatRは、2025年7月2日のv13.0.0以降、RPL-1.5・無償のCommunity版・商用の3択になりました。新規ならMediatRなしで書き始めます。
- 機能数が少なく分割の利益がないアプリでは採用を見送ります。複数機能が複雑な業務ルールを共有する場合は、そのルールをドメインモデルへ集約したうえでVSAとの併用を判断します。
バーティカルスライスの4つの意味と使い分け
検索クエリ「バーティカルスライス」「vertical slice」は、文脈によって別物を指します。ゲーム開発とWeb開発の会話が交わる場面では、設計の話をしているつもりで相手はデモの話をしていた、という取り違えが起こります。まず全体を表で並べます。
| 分野 | 指すもの | できあがるもの |
|---|---|---|
| ゲーム開発 | 完成時の体験を示す一部分 | 遊べる代表的な部分のビルド |
| アジャイル | ストーリーの分割方法 | 画面からDBまで動く小機能 |
| 書籍(Seemann) | 実装を始める順序 | 入力から永続化まで通る1機能 |
| 設計手法(VSA) | 機能単位の責務と依存の設計 | 機能名のディレクトリ |
ゲーム開発:出資判断に使うプレイ可能な部分
ゲーム開発でバーティカルスライスと言えば、グラフィック・サウンド・UI・操作感を含む完成時の体験を示すために作る、プレイ可能な代表的部分を指します。分量はゲームに応じて決まり、1ステージに限定されません。全体のごく一部の分量でも、完成後の体験をそのまま判断できる状態にすることが目的です。パブリッシャーへの出資申請やチーム内の方向性確定に使われます。ソースコードの配置とは無関係で、成果物の作り方を指す言葉です。
アジャイル:全レイヤを貫くユーザーストーリー分割
アジャイル開発の垂直分割は、ユーザーストーリーを「DB設計」「API実装」「画面実装」と工程で割らず、「ユーザーが1件検索できる」のように画面からDBまでを貫く単位で割る考え方です。水平分割ではスプリント終了時に動くものが何も無いのに対し、垂直分割なら小さくても動作するソフトウェアが残ります。VSAで作る Features ディレクトリは、この分割単位をそのままディレクトリ名にしたものです。ユーザーストーリーと機能の境界をそろえると、変更対象のディレクトリを特定しやすくなります。ただし、改修チケットと既存ディレクトリが常に1対1になるわけではありません。
書籍『脳に収まるコードの書き方』:動くソフトウェアから始める実装順序
Mark Seemannの『脳に収まるコードの書き方』(原書 Code That Fits in Your Head、オライリー・ジャパン、2024年6月18日発売)は、4章の章題が「バーティカルスライス」です。目次は「4.1 動くソフトウェアから始める」「4.1.1 データ入力からデータ永続化まで」「4.2 ウォーキングスケルトン」「4.3 外側から内側へ(アウトサイドイン)」「4.4 バーティカルスライスを完成させる」と続きます。ここでのバーティカルスライスは、入力から永続化まで実際に動く最小の機能を最初に通す、という実装の進め方です。同じ章の節にはリポジトリインターフェイスやDTOとドメインモデルの話が並んでおり、層を廃する設計手法としてのVSAとは別の話です。日本語の技術書でこの言葉に出会った場合は、まずこの用法を疑ってください。
設計手法:機能単位でコードを束ねるVertical Slice Architecture
この記事の主題であるVSAは、Jimmy Bogardが名付けた、機能ごとに関心事をまとめて結合を制御する設計手法です。ゲーム開発の用法とは目的が異なり、アジャイルの垂直分割やSeemannの実装順序とは「入口から永続化までを1本で貫く」という発想を共有しています。日本語では垂直スライスアーキテクチャ、バーティカルスライスアーキテクチャと表記されます。以降はすべてこの意味で使います。
Vertical Slice Architectureの定義と提唱の経緯
VSAは、リクエスト1本を単位として、そのリクエストが必要とするすべての関心事を1か所にまとめる構造です。「注文取消」というスライスの中に、入力の受け取り・検証・業務ルールの判定・データ更新・レスポンス生成が同居します。同じ機能のコードが層をまたいで散らばりません。
名前を定着させたのは、Jimmy Bogardが2018年4月19日に公開した記事「Vertical Slice Architecture」です。中心的な主張は「Minimize coupling between slices, and maximize coupling in a slice(スライス間の結合を最小化し、スライス内の結合を最大化する)」の一文です。あわせて「most abstractions melt away(抽象化のほとんどは溶けて消える)」とし、層をまたぐ共通のリポジトリ・サービス・コントローラといった抽象は不要になると述べています。
起源を2018年と書く解説を見かけますが、これは記事化された年です。原型は講演「SOLID Architecture in Slices not Layers」で、NDC Osloでの登壇動画がLos Techiesのブログで2015年7月2日に公開告知され、Øredev 2016でも同じ演題で登壇しています。なお2018年の記事にはコード例が無く、概念図と議論だけで構成されています。実装の具体は各自が補う必要があります。
レイヤード・クリーンアーキテクチャとの構造比較
比較するのは、コードを分割する軸と、変更時に確認する範囲です。
| 観点 | レイヤード(N層) | クリーンアーキテクチャ | Vertical Slice Architecture |
|---|---|---|---|
| 分割の基準 | 技術的な役割 | 依存方向とドメイン | 機能(リクエスト) |
| 1機能の変更範囲 | 全層を横断 | 全層+境界の定義 | スライス内への局所化を重視 |
| 抽象化の量 | 中 | 多 | 少 |
| 重複コード | 抑制 | 抑制 | 条件付きで許容 |
| 影響範囲の把握 | 横断調査が必要 | 横断調査が必要 | ディレクトリ単位 |
| 相性のよい状況 | 要件が安定 | ドメインが複雑 | 機能追加が頻繁 |
クリーンアーキテクチャは依存の向きを内側へ固定し、DBやフレームワークの差し替えに強い構造を作ります。VSAが最適化しているのは差し替え耐性ではなく変更速度です。読み取り専用の一覧APIにまでリポジトリとユースケースの抽象を通す構成が過剰だと感じたなら、その部分にVSAの発想が効きます。依存方向を軸にした設計思想はクリーンアーキテクチャとは?4層と依存性ルールを原典で解説し、いらない場面の判断基準まで、外部との境界をポートとアダプタで切る手法はヘキサゴナルアーキテクチャとは?ポートアンドアダプターの仕組みとクリーン・オニオンとの違いで扱っています。
実装例:フォルダ構成とスライス1本のコード
VSAは概念より配置を見たほうが速く理解できます。C#とGoの2言語で、注文取消という1スライスを示します。どちらも2026年10月13日に手元でビルドし、支払済みの注文は204、出荷済みは409、存在しないIDは404が返ることを確認したコードです。Goはストア障害時に500を返すことも確認しました。
フォルダ構成:機能名で切るディレクトリ配置
技術的な役割名(Controllers、Services、Repositories)のディレクトリを作らず、機能名で切ります。
src/
Features/
Orders/
PlaceOrder.cs
CancelOrder.cs
GetOrderDetail.cs
Shipping/
CreateShipment.cs
Shared/
Persistence/AppDbContext.cs
Behaviors/RequestLogging.cs
注文取消だけの処理変更なら、主な確認先をCancelOrder.csに絞れます。ただし、共有モデルやDBスキーマに及ぶ変更では関連ファイルも確認します。レイヤード構成なら OrdersController・OrderService・OrderRepository・DTO を順に追うことになります。この差が、VSAが変更速度で有利になる理由の実体です。Shared に置くのは、機能に依存しない基盤だけに限定します。
C#(ASP.NET Core Minimal API):エンドポイントと業務ルールの1ファイル集約
リクエスト型・エンドポイント登録・処理本体を1ファイルに置きます。MediatRなどのライブラリは必須ではありません。
// src/Features/Orders/CancelOrder.cs
using Shop.Shared.Persistence;
namespace Shop.Features.Orders;
public sealed record CancelOrderRequest(string Reason);
public static class CancelOrder
{
public static void Map(IEndpointRouteBuilder app) =>
app.MapPost("/orders/{id:guid}/cancel", Handle);
private static async Task<IResult> Handle(
Guid id,
CancelOrderRequest body,
AppDbContext db,
CancellationToken ct)
{
var order = await db.Orders.FindAsync(new object[] { id }, ct);
if (order is null) return Results.NotFound();
if (order.Status == OrderStatus.Shipped)
return Results.Conflict("出荷済みの注文は取り消せません");
order.Status = OrderStatus.Cancelled;
order.CancelReason = body.Reason;
await db.SaveChangesAsync(ct);
return Results.NoContent();
}
}
出荷済みなら取り消せないという業務ルールが、エンドポイントの定義と同じ画面に見えている点がVSAの狙いです。ルールを探してサービス層を辿る手間が消えます。エンティティ Order と列挙型 OrderStatus、AppDbContext は Shared 側に置き、Program.cs では AddDbContext で登録したうえで CancelOrder.Map(app) を呼びます。検証は .NET SDK 10.0.401 と EF Core InMemory 10.0.12 で行いました。
この「リクエスト型・エンドポイント・レスポンス」を1組で扱う形は、REPR(Request-EndPoint-Response)パターンと呼ばれます。エンドポイント1本を1クラスにする FastEndpoints などのライブラリはこの形を前提にしており、VSAのスライスと単位がそろいます。テンプレートから始めたい場合は、MITライセンスの Hona/VerticalSliceArchitecture(GitHubスター127)が参考になります。ただし最終コミットは2024年10月4日なので、最新の.NETに追随しているかを取り込み前に確認してください。
Go:機能単位パッケージによる同型構造
VSAは.NET専用の手法ではありません。Goでは機能名のパッケージがそのままスライスになります。
// internal/feature/cancelorder/cancelorder.go
package cancelorder
import (
"context"
"encoding/json"
"errors"
"net/http"
)
const (
StatusShipped = "shipped"
StatusCancelled = "cancelled"
)
// ErrOrderNotFound は store が注文を見つけられないときに返す
var ErrOrderNotFound = errors.New("order not found")
type Order struct {
ID string
Status string
CancelReason string
}
type request struct {
Reason string
}
type store interface {
Load(ctx context.Context, id string) (Order, error)
Save(ctx context.Context, o Order) error
}
func Handle(s store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
o, err := s.Load(r.Context(), r.PathValue("id"))
if errors.Is(err, ErrOrderNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, "internal", http.StatusInternalServerError)
return
}
if o.Status == StatusShipped {
http.Error(w, "already shipped", http.StatusConflict)
return
}
o.Status = StatusCancelled
o.CancelReason = req.Reason
if err := s.Save(r.Context(), o); err != nil {
http.Error(w, "internal", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
store インターフェースを共通パッケージではなくスライス側に置いている点に注目してください。利用側がインターフェースを定義するGoの慣習と、スライス内で完結させるVSAの原則が一致します。store の実装は、注文が無いときに ErrOrderNotFound を返す約束です。それ以外のエラー(DB接続の失敗など)は500にし、障害を「注文が無い」と取り違えないようにしています。登録側は mux.Handle(“POST /orders/{id}/cancel”, cancelorder.Handle(s)) の1行です。メソッド指定つきのパターンと r.PathValue は Go 1.22 のリリースノートで標準の net/http に入った機能で、ルータライブラリを入れずにこの構造を組めます。go.mod の go 行が古いままだと動きません。go 1.21 に書き換えて go test を実行すると、ビルド前の検査で「http.(*Request).PathValue requires go1.22 or later (module is go1.21)」と出て止まりました。go 行が 1.22 以上かを先に確認してください。
スライス間の依存禁止テストと検出範囲
VSAの原則は「スライス間の結合を最小化する」ですが、運用を続けると、隣のスライスのリクエスト型や定数を「ちょっと借りる」参照が必ず紛れ込みます。1本入ると、そのスライスを変更したときに別機能が壊れる経路ができ、VSAの利点だった変更範囲の予測が崩れます。この約束はコードレビューでは守り切れません。テストにして、CIで落とします。
.NET:ArchUnitNETのSlicesルールで一括検査
.NETでは ArchUnitNET(Apache-2.0、2026年8月公開の0.13.4が最新)に、名前空間の一部をスライス名として扱うルールがあります。Matching に渡した「Shop.Features.(*)」の (*) に当たる部分がスライス名になり、スライス同士が互いに依存しないことを1ルールで書けます。機能フォルダを追加してもテストを書き足す必要はありません。
// tests/Shop.ArchTests/SliceRules.cs
// dotnet add package TngTech.ArchUnitNET.xUnit --version 0.13.4
using ArchUnitNET.Domain;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using static ArchUnitNET.Fluent.Slices.SliceRuleDefinition;
namespace Shop.ArchTests;
public class SliceRules
{
private static readonly Architecture Architecture =
new ArchLoader()
.LoadAssemblies(typeof(Shop.Features.Orders.CancelOrder).Assembly)
.Build();
[Fact]
public void Features配下のスライス同士は参照しない() =>
Slices()
.Matching("Shop.Features.(*)")
.Should()
.NotDependOnEachOther()
.Check(Architecture);
}
xunit 2.9.3 のテストプロジェクトで実行すると、違反がない状態では合格します。Shipping スライスから Orders スライスの CancelOrderRequest を参照する1行を足すと、次のメッセージで失敗しました。どのスライスがどの型を参照したかまで出るため、修正箇所を探す手間がかかりません。
"Slices matching "Shop.Features.(*)" should not depend on each other" failed:
Shipping does depend on other slices:
Shipping -> Orders
Shop.Features.Shipping.CreateShipment -> Shop.Features.Orders.CancelOrderRequest
Shared への依存はこのルールに引っかかりません。共通基盤への参照は許し、機能同士の参照だけを禁じるという、VSAの線引きそのものをテストにした形です。xUnit v3 を使う場合は、別パッケージの TngTech.ArchUnitNET.xUnitV3 を選びます。
検出範囲には2つ癖があります。実測で確かめた結果です。1つ目に、同じ名前空間の中の参照は検出しません。Orders 名前空間にある PlaceOrder から CancelOrderRequest を参照しても合格しました。この例のテストが守るのは Orders と Shipping という機能フォルダ同士の境界で、リクエスト単位の独立までは保証しません。リクエスト単位で禁じたいなら、Features/Orders/CancelOrder/ のように1リクエスト1名前空間に分けます。2つ目に、(*) は下位の名前空間まで含めてスライス名にします。Shop.Features.Orders.Internal に置いた補助クラスが Orders の型を参照すると「Orders.Internal -> Orders」の違反になりました。ArchUnitNETのIssue #468で報告されている、(*) と (**) が区別されない問題に当たります。スライスの中に下位の名前空間を作らない構成にしておけば、この誤検知は起きません。
Go:標準ライブラリだけで書くimport検査
Goは追加ライブラリなしで書けます。internal/feature 以下のディレクトリを再帰的にたどり、go/build で各パッケージの import を読みます。import 先が別のスライス(internal/feature 直下の別ディレクトリ)なら失敗させます。
// internal/feature/slices_test.go
package feature_test
import (
"go/build"
"io/fs"
"path/filepath"
"strings"
"testing"
)
const prefix = "example.com/shop/internal/feature/"
// スライス名=internal/feature 直下のディレクトリ名
func sliceOf(path string) string {
return strings.SplitN(filepath.ToSlash(path), "/", 2)[0]
}
// スライス同士の import を禁止する(子パッケージも再帰的に検査)
func TestSlicesDoNotImportEachOther(t *testing.T) {
err := filepath.WalkDir(".", func(dir string, d fs.DirEntry, err error) error {
if err != nil || !d.IsDir() || dir == "." {
return err
}
pkg, err := build.ImportDir(dir, 0)
if _, ok := err.(*build.NoGoError); ok {
return nil
}
if err != nil {
return err
}
for _, imp := range pkg.Imports {
rest, ok := strings.CutPrefix(imp, prefix)
if ok && sliceOf(rest) != sliceOf(dir) {
t.Errorf("%s が別スライスの %s を import しています", dir, imp)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
Go 1.27.2 で、placeorder の子パッケージ placeorder/nested から cancelorder を import すると「placeorder/nested が別スライスの example.com/shop/internal/feature/cancelorder を import しています」で失敗しました。同じスライスの中で placeorder/nested から placeorder を import するのは合格します。ただし検査するのは、現在のビルド条件で有効な通常ソースの import だけです。pkg.Imports はテストファイルの import を含まず、別OS向けなどビルドタグで外れるファイルも読みません。Go の internal ディレクトリは、その親ディレクトリを根とするツリーの外からの参照を制限しますが、internal 配下の兄弟パッケージ同士の import は止めない点に注意してください。依存ルールの検査を go-arch-lint などの専用ツールに任せる選択肢はgo-cleanarchとは?クリーンアーキテクチャの依存ルールをCIで検査する方法とgo-arch-lint・ArchUnitの使い分けで比べています。
スライスをまたぐ共通処理と重複コードの線引き
配置のルールは単純でも、共通処理をどこに置くかの判断は毎回発生します。
横断的関心事:スライスの外に置く判断基準
認証・ロギング・トランザクション・入力検証は、各スライスへ書き写すと保守不能になります。これらはスライスの中ではなく実行経路に置きます。ASP.NET Coreならミドルウェアまたはエンドポイントフィルタ、Goならハンドラを包むミドルウェア関数です。判断基準は仕様書への記載有無ではなく、複数機能へ共通に適用する仕組みか、個別機能の業務上の判断かです。「出荷済みは取り消せない」は取消機能の業務判断として扱います。監査ログの記録機構は共通化できますが、記録対象や内容は各機能の要件に従って決めます。
重複コードの許容範囲と業務ルールの共通化基準
VSAは重複を許容しますが、無条件ではありません。許してよいのは、たまたま今は同じ形をしているだけで、別々の理由で変わりうるコードです。注文一覧と管理画面一覧のレスポンス組み立てが似ていても、片方だけ項目追加されることは普通に起きるため、共通化するとかえって壊れます。
逆に、複数スライスが同一の業務ルールを実装している場合は共通化します。金額計算や与信判定のロジックが3か所にあって常に同時に修正されるなら、それは重複ではなく共有すべきドメインの知識です。片方だけを変更したくなる場面が想像できるなら重複のまま残し、常に両方を同時に直すことになるなら共通化する。この線引きを曖昧にしたまま「VSAだから重複してよい」と進めると、修正漏れによる不具合が積み上がります。共通化したルールはスライスから参照される側の Shared かドメイン層に置くので、上の依存禁止テストとも衝突しません。
MediatR前提の.NET実装で2025年に変わったライセンス条件
MediatRを前提にしたVSAの実装例は広く使われていますが、その前提は2025年に変わりました。導入前に確認してください。
MediatRは2025年7月2日にv13.0.0を公開し、Apacheライセンスから商用とOSSのデュアルライセンスへ移りました。提供元はJimmy Bogardが設立したLucky Penny Softwareです。v13.0.0以降の選択肢は3つあります。GitHub公開ソースのReciprocal Public License 1.5(RPL-1.5)を受諾する(公開義務と引き換えに企業規模を問わず無償)、無償のCommunity版を使う、有償の商用ライセンスを買う、のいずれかです。v12.5.0(2025年4月1日公開)以前は従来のApacheライセンスのままです。その後も開発は続いており、2025年12月3日のv14.0.0で.NET 10に対応し、2026年10月時点の最新はv14.2.0(2026年7月2日公開)です。
Community版は、公式サイトに書かれた財務条件(年間総収益または非営利団体の年間予算が500万USD未満、外部からの資本受入が累計1,000万USD以下)を満たし、かつLucky Pennyのよくある質問にある除外(政府・準政府機関、大学など高等教育機関が組織運用ソフトウェアとして使う用途)に当たらない場合に使えます。売上基準だけを見て無償と判断すると、資本受入の条件で外れる場合があります。一方で授業・学生の課題・個人学習、それに大学運営に使わない学術研究は、所属先を問わず対象になるとライセンス変更の説明に明記されています。
新規に.NETでVSAを始めるなら、まずMediatRなしで書いてください。上のC#の例のとおり、Minimal APIの静的メソッド1本でスライスは成立します。MediatRの価値の中心はパイプライン動作(検証・ロギングの共通処理)ですが、これはエンドポイントフィルタでも実現できます。上の3つの選択肢のいずれかに該当し、かつチームがMediatRに習熟している場合に限り採用する、という順序が安全です。v12.5.0に固定して古いライセンスに留まる運用は、以後の修正を受け取れなくなるため避けてください。
AIコーディング支援とスライス境界
2026年に入り、Bogard自身がVSAをAI支援開発の文脈で語り始めました。2026年7月23日の告知記事で、無料ウェビナー「Vertical Slice Architecture: Effective Guardrails for AI Development」を8月20日に開催するとし、AIツールはコードを速く作れても変更しやすく保守しやすいコードを作るとは限らず、長いレビューの往復や不具合を生んでいると書いています。そのうえで、副作用と結合を減らすことでVSAがその往復を短くできる、という立場を示しました。
実務に落とすと、効くのは次の2点です。1つは指示の単位がそろうことです。「注文取消に理由の必須チェックを足して」という依頼に対し、処理本体の変更範囲を Features/Orders/CancelOrder.cs に限定でき、テストや共有モデルの変更を伴わない場合には、生成された差分を読む量が1ファイルに収まります。もう1つは、上の依存禁止テストがそのまま機械の見張り役になることです。エージェントが隣のスライスの型を借りる近道をしても、CIが落ちて差し戻せます。逆に言えば、テストで境界を固定していないVSAは、AIに書かせる量が増えるほど崩れやすくなります。
適用判断:向くプロジェクトと見送るべき条件
効果が出る条件:機能追加が続き、担当が分かれるプロジェクト
VSAが効くのは、機能単位の追加・変更が継続し、複数人が別々の機能を並行して触る状況です。担当者ごとに触るディレクトリが分かれるため、コンフリクトが減ります。効果は「直近1か月に追加した機能の変更で何ファイル触ったか」という数字で測れます。既存の大規模モノリスを機能単位で切り出していく移行期にも向きます。切り出しの前段としてモジュール境界を引く考え方はモジュラモノリスとは?モジュール境界の設計とDB分割・依存検証の実装が参考になります。業務システムをゼロから組み直す段階で機能単位の構成を最初から選びたい場合は、フルスクラッチ開発の設計工程で相談いただけます。
採用を見送る条件:分割の利益不足・共有ルールの分散・改善体制の欠如
次の場合、VSAは採用しません。
- エンドポイントが十数本程度で今後も増えない小規模アプリ。ディレクトリを機能で割る利益より、構造を説明するコストが上回ります。
- 保険料計算や在庫引当などの共有ルールを各スライスへ複製し、共通のドメインモデルへ集約できない設計・体制。スライス内で完結させると同じルールが分散し、修正漏れが起きます。この場合はドメイン駆動設計(DDD)の考え方で共有ルールをドメインモデルへ集約する作業を先に行い、VSAの導入はその後にします。
- チームがリファクタリングに不慣れな場合。Bogard自身が原記事で、複雑なロジックをドメインへ押し出す判断が必要になると書いており、VSAは「まず素直に書き、必要になったら共通化する」段階的な改善を前提とします。最初に書いた形を誰も直さない体制では重複が放置されます。
1つ目と2つ目は、部分適用で折り合いをつけられます。参照系だけVSA、更新系はドメインモデル経由という構成は現実的な落としどころです。3つ目は体制側の問題なので、部分適用では解決しません。
モジュラモノリス・CQRS・フロントエンド設計との関係
モジュラモノリス:粒度の違いと併用の可否
モジュラモノリスはデプロイ単位を1つに保ったままモジュール境界を引く手法で、境界の粒度は「注文」「在庫」といった業務ドメイン単位です。VSAが決めるのはその内側、リクエスト単位の配置です。粒度が異なるため、モジュール内をVSAで構成する組み合わせは矛盾しません。将来的にマイクロサービスへ分割する計画があるなら、境界の考え方はマイクロサービスとは?モノリスとの違い・メリット・デメリットと選び方、モジュラモノリスまで整理で確認できます。
CQRS:更新系と参照系での非対称な実装
Bogardの原典では、リクエストを更新系と参照系に分けることで、VSAが自然にCQRSにつながると説明されています。更新系スライスはドメインモデルを経由し、参照系スライスは SELECT id, status, total FROM orders のように必要な列だけを直接取得してレスポンスに詰める、という非対称な実装が許容されます。スライスごとにデータアクセスの手段を変えてよいので、更新系はEF Core、参照系はDapperで生SQL、という分担も取れます。読み取り用と書き込み用のモデルを分ける発想の詳細はCQRS(コマンドクエリ責務分離)とは?仕組み・Read Model/Write Model・Query Serviceと実装パターンを解説を参照してください。
フロントエンド:Feature-Sliced Designとの対応関係
フロントエンドでVSAに相当するのがFeature-Sliced Designです。components・hooks・utilsという技術的な分類でディレクトリを切る代わりに、機能単位のスライスへ束ねます。Layers・Slices・Segmentsという3階層で責務範囲を規定する点が特徴で、命名規約が明文化されている分だけVSAより導入手順が具体的です。Feature-Sliced Designとは?Layers・Slices・Segmentsで学ぶフロントエンド設計で構造を確認できます。
よくある質問
VSAは何の略ですか?
Vertical Slice Architectureの略です。日本語では垂直スライスアーキテクチャ、バーティカルスライスアーキテクチャと表記されます。検索では Vertical Slices Architecture、Slice Architecture、Vertical Architecture といった表記ゆれも見られますが、指している構造は同じです。なおVSAという略号は他分野でも使われており、IT運用管理ツールの Kaseya VSA(Virtual System Administrator)が代表例です。設計手法の情報を探すときは文脈語を添えて検索すると目的の情報に絞れます。
スライスの粒度はどの単位で区切りますか?
APIのエンドポイント1本、または画面上の1操作を基準にします。「注文を取り消す」「注文一覧を取得する」はそれぞれ別スライスです。CRUDでまとめて OrderFeature のような塊にすると、レイヤードのサービスクラスと同じ肥大化が起きます。判断に迷ったら、そのコードを変更する理由が1つかどうかを確認してください。取消の仕様変更と一覧の項目追加は別々に発生するため、分けます。逆に、常に同時に変わるものは1スライスにまとめて構いません。
既存のレイヤード構成から段階的に移行できますか?
できます。全面書き換えは不要で、新規に追加する機能からスライス形式で書き、既存の層はそのまま残す進め方が現実的です。Features ディレクトリを新設し、そこに追加したファイルだけが自己完結している状態を作ります。既存のリポジトリやサービスをスライスから呼び出しても構いません。移行の進み具合は、追加機能の修正対象がFeatures配下へどれだけ集約されたかと、変更ファイル数の推移を分けて確認します。Features 配下の変更ファイル数が減らないなら、スライスの中で既存層に依存しすぎている可能性があります。
Vertical Slice Architectureのテストはどう書きますか?
動作の確認はスライス単位の統合テストを主軸にします。1スライスが入口から永続化まで含むため、HTTPリクエストを投げて結果を検証する形が自然で、モックの量も減ります。ASP.NET CoreではWebApplicationFactoryとテスト用HttpClient、Goではhttptest.NewRecorderとNewRequestでHTTP処理を検証できます。永続化までの統合テストにする場合は、ストアを実際のテスト用DBへ接続します。層ごとのユニットテストを機械的に用意すると、抽象を減らしたVSAの利点が失われます。ユニットテストは金額計算のような入出力が明確なロジックに絞り、構造の約束は前述の依存禁止テストで別に守ります。
マイクロサービス化の前段として使えますか?
有効です。機能単位でコードが分離されている状態は、切り出し候補の特定と分離作業を単純にします。ただしVSAのスライスをそのままサービス境界にはしません。エンドポイント1本ごとにサービスを作れば分散モノリスになります。まずVSAでコードを整理し、次に複数スライスをまとめた業務ドメイン単位でモジュール境界を引き、最後に分離する、という順序で進めます。