第40章:総合演習(BC分割→Context Map→実装修正)🎮✅
この章で作るもの 🎁✨
ミニEC題材で、次の成果物をぜんぶ1本につなげて完成させます🧩💕
- ✅ **分割前コード(地雷入り)**を読んで、衝突ポイントを発見🧨
- ✅ BC案を決める(用語集つき)📒
- ✅ Context Mapで関係を1枚にする🗺️🤝
- ✅ C#で境界を守る(DTO / ACL / internal)🔒💻
- ✅ テストで境界の崩壊を検知できるようにする🧪✅
- ✅ 最後にレビュー観点チェックで合格判定🎓✨
40-1 演習の前提ストーリー ミニECの世界 🛒📦💳🚚
登場する機能はこの4つだけに絞ります(絞るの大事💡)
- 受注:注文を作る・状態を進める 📦
- 在庫:在庫を引当てる・戻す 🧺
- 請求:支払いの与信・確定 💳
- 配送:送り状を作る・出荷する 🚚
ここで大事なのは、「注文」「顧客」「金額」「住所」みたいな言葉が、部署ごとに意味がズレるところです😵💫 このズレを、BCで分けて守るのがゴールです💪✨
40-2 まず分割前コードを読む 地雷探しゲーム 🧨🎮
分割前の悪い例 全部入り OrderService 💣
- 受注・在庫・請求・配送が1クラスで混ざってます
- 「注文ステータス」が在庫/請求/配送の都合で増殖します
- “便利そう”が一番危ないやつ😇
// ❌ 分割前(地雷):責務が全部混ざってる
public class OrderService
{
private readonly DbConnection _db;
private readonly PaymentApi _payment;
private readonly ShippingApi _shipping;
public async Task PlaceOrderAsync(Guid customerId, List<(string sku, int qty)> lines, string address)
{
// 1) 在庫チェック(ここからして受注の責務じゃない)
foreach (var (sku, qty) in lines)
{
var stock = await _db.QuerySingleAsync<int>("SELECT Stock FROM Items WHERE Sku=@sku", new { sku });
if (stock < qty) throw new Exception("OutOfStock");
}
// 2) 注文作成
var orderId = Guid.NewGuid();
await _db.ExecuteAsync("INSERT INTO Orders(Id, CustomerId, Status, Address) VALUES(@id,@c,'NEW',@a)",
new { id = orderId, c = customerId, a = address });
// 3) 在庫引当(DB直で減らす)
foreach (var (sku, qty) in lines)
{
await _db.ExecuteAsync("UPDATE Items SET Stock = Stock - @q WHERE Sku=@sku", new { sku, q = qty });
}
// 4) 支払い(与信→確定を一気に)
var total = await _db.QuerySingleAsync<decimal>("SELECT SUM(Price*Qty) FROM OrderLines WHERE OrderId=@id", new { id = orderId });
var paymentResult = await _payment.ChargeAsync(customerId, total);
if (!paymentResult.Success) throw new Exception("PaymentFailed");
// 5) 配送手配
var tracking = await _shipping.CreateShipmentAsync(address, lines);
// 6) 状態更新(なんか全部ここ…)
await _db.ExecuteAsync("UPDATE Orders SET Status='SHIPPING', Tracking=@t WHERE Id=@id", new { id = orderId, t = tracking });
}
}
地雷チェックリスト 読んで印を付けよう 🖊️👀
次を見つけたら、コードにコメントで印を付けます✅
- ✅ “注文”の中に在庫のルールがいる
- ✅ “注文”の中に請求のルールがいる
- ✅ “注文”の中に配送のルールがいる
- ✅ “Address”が1個しかない(配送住所?請求先?😇)
- ✅ 例外が雑(OutOfStock / PaymentFailed が直投げ)
- ✅ DBテーブル共有っぽい(Items と Orders が直結)
40-3 BC案を決める まずは4分割でOK 🧩✨
BC案 この章ではこれで進めるよ 🧭
- OrderManagement:受注の意思決定(注文を作る、状態を進める)📦
- Inventory:在庫の真実(引当/戻し/在庫数)🧺
- Billing:お金の真実(与信/確定/返金)💳
- Shipping:配送の真実(送り状/出荷)🚚
.NET 10 は 2025年11月11日にリリースされた LTS で、サポートは 2028年11月14日までです。(Microsoft) C# 14 は .NET 10 SDK と組み合わせて試せます。(Microsoft Learn)
用語集ミニ 同名でもBCで意味が変わる例 📒🌀
ここ、超重要です🧡(衝突を“見える化”)
-
注文
- OrderManagement:顧客が「買う」と決めた意思決定の記録
- Shipping:配送する荷物の単位(送り状・梱包単位に寄る)
- Billing:請求の対象(確定した金額・税計算ルールの単位)
-
住所
- Shipping:配送先(届ける場所)
- Billing:請求先(カード/請求書の名義と整合)
40-4 Context Mapを1枚にする 🗺️🤝
この章の関係の決め方 シンプル版 🌸
-
OrderManagement が “進行役” になって
- 在庫へ「引当していい?」
- 請求へ「与信していい?」
- 配送へ「出荷していい?」 を順番にお願いする感じです🙋♀️
図 依存の向きだけは絶対に描く ✍️
[Inventory] <--(ACL/DTO)-- [OrderManagement] --(ACL/DTO)--> [Billing]
|
+--(ACL/DTO)--> [Shipping]
- どの矢印にも ACL/DTO を入れてるのがポイント🛡️✨
- つまり「相手の型(ドメイン)をそのまま持ち込まない」🙅♀️
最近は GitHub の Copilot などで“エージェント的にタスクを進める”支援も増えてるので、図→境界→実装の順で固めるとAIとも相性が良いです🤖🧠(The Verge)
40-5 実装 プロジェクト分割と参照ルール 🔒🏗️
フォルダとプロジェクト構成案 📁✨
/src
/OrderManagement
OrderManagement.Domain
OrderManagement.Application
OrderManagement.Contracts (公開するDTOだけ)
OrderManagement.Infrastructure (ACLの実装はここ)
/Inventory
Inventory.Domain
Inventory.Application
/Billing
Billing.Domain
Billing.Application
/Shipping
Shipping.Domain
Shipping.Application
/tests
OrderManagement.Tests
Inventory.Tests
Billing.Tests
Shipping.Tests
EndToEnd.Tests
参照ルール これだけ守れば勝ち 🏆
- Application は Domain を参照してOK ✅
- Infrastructure は Application を参照してOK ✅
- 他BCのDomainは参照しない ❌
- 境界越えは Contracts(DTO)+ACL だけでやる ✅
Visual Studio 2022 は 2026年1月時点で 17.14 系が提供されています。(Microsoft Learn) GitHub Copilot の統合拡張は Visual Studio 17.10 以降で使えます。(Visual Studio)
40-6 境界越え DTOとACLを作る 📨🛡️
公開DTO OrderManagement.Contracts 側 📦
「相手に渡す最小限」だけにします🥗✨
namespace OrderManagement.Contracts;
public sealed record ReserveStockRequest(
Guid OrderId,
IReadOnlyList<ReserveStockLine> Lines
);
public sealed record ReserveStockLine(string Sku, int Quantity);
public sealed record ReserveStockResponse(
bool Success,
string? FailureReason
);
ACL OrderManagement.Infrastructure 側 🔄
- OrderManagement は IInventoryGateway だけ知ってる
- 中で Inventory の都合に合わせて変換する(翻訳)📖✨
using OrderManagement.Contracts;
namespace OrderManagement.Infrastructure.InventoryAcl;
public interface IInventoryGateway
{
Task<ReserveStockResponse> ReserveAsync(ReserveStockRequest request, CancellationToken ct);
}
// ✅ ACL実装(ここだけが Inventory に触ってよい)
public sealed class InventoryGateway : IInventoryGateway
{
private readonly Inventory.Application.InventoryAppService _inventory;
public InventoryGateway(Inventory.Application.InventoryAppService inventory)
=> _inventory = inventory;
public async Task<ReserveStockResponse> ReserveAsync(ReserveStockRequest request, CancellationToken ct)
{
// DTO → Inventory側のコマンドへ変換(翻訳)
var cmd = new Inventory.Application.Commands.ReserveCommand(
request.OrderId,
request.Lines.Select(x => new Inventory.Application.Commands.ReserveLine(x.Sku, x.Quantity)).ToList()
);
var result = await _inventory.ReserveAsync(cmd, ct);
// Inventory側の結果 → DTO に戻す
return new ReserveStockResponse(result.Success, result.FailureReason);
}
}
ポイント:Inventory のコマンド型は 外へ出ない(internal でもOK)🔒 “相手の揺れ”を吸う場所が ACL です🛡️✨
40-7 分割後の流れ OrderManagementはオーケストラ 🎻✨

Application Service の例 🎼
public sealed class PlaceOrderHandler
{
private readonly IInventoryGateway _inventory;
private readonly IBillingGateway _billing;
private readonly IShippingGateway _shipping;
private readonly IOrderRepository _orders;
public async Task<Guid> HandleAsync(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId, cmd.Lines, cmd.ShippingAddress);
// 1) 在庫引当
var stock = await _inventory.ReserveAsync(order.ToReserveRequest(), ct);
if (!stock.Success) throw new DomainException($"OutOfStock: {stock.FailureReason}");
// 2) 与信
var auth = await _billing.AuthorizeAsync(order.ToAuthorizeRequest(), ct);
if (!auth.Success)
{
await _inventory.ReleaseAsync(order.ToReleaseRequest(), ct);
throw new DomainException($"PaymentDenied: {auth.FailureReason}");
}
// 3) 配送手配
var ship = await _shipping.CreateShipmentAsync(order.ToShipmentRequest(), ct);
order.AttachShipment(ship.TrackingNumber);
await _orders.SaveAsync(order, ct);
return order.Id;
}
}
ここで境界が守れてる理由 💡
- OrderManagement の中に「在庫の在り方」は入ってない🧺❌
- OrderManagement の中に「課金の仕様」は入ってない💳❌
- OrderManagement の中に「配送の事情」は入ってない🚚❌
- 代わりに「お願いする契約(DTO)」だけがある📨✅
40-8 テスト 境界が崩れたら赤くなるようにする 🧪🚨
1) Domain単体テスト まずは各BCで勝つ 🏅
- OrderManagement.Domain:注文の不変条件(例:合計がマイナスにならない)✅
- Inventory.Domain:引当ルール(例:在庫未満は失敗)✅
- Billing.Domain:与信の状態遷移(例:確定前に返金しない)✅
2) ACLテスト 翻訳が壊れたら死ぬ😇
- DTOの必須項目が落ちてないか
- 値の単位(税/通貨/丸め)がズレてないか
- null/欠損時の扱いが合ってるか
3) End-to-End テスト 最短で1本通す 🎯
- 正常系:注文→在庫引当→与信→配送→保存 ✅
- 異常系:与信NGなら在庫戻しが動く ✅
- 異常系:在庫NGなら請求に触らない ✅
40-9 最終レビュー 合格チェックリスト ✅🎓✨

言葉の一貫性 📒
- ✅ “注文” “住所” “金額” の意味がBC内でブレてない
- ✅ DTOは “公開する言葉” になってる(曖昧な名前がない)
依存の向き ➡️
- ✅ 他BCのDomain参照がない
- ✅ 参照できるのは Contracts と Gatewayインターフェースだけ
共有の扱い 🧱
- ✅ 共有テーブル直読みがない(やるなら所有者ルールがある)
- ✅ “共通便利クラス” が増殖してない
テストで守れてる 🧪
- ✅ ACLの翻訳テストがある
- ✅ 失敗時の巻き戻し(在庫戻し等)がE2Eで検証されてる
40-10 お助けAIプロンプト この章専用 🤖✨
コピペして使える形にしてあります📋💕
あなたはDDD/Bounded Contextの先生です。
次の分割前コードを読み、(1)衝突している用語 (2)責務の混在 (3)BC分割案(3〜6個)を提案して。
各BCごとに「目的を1行」「主要エンティティ/値オブジェクト候補」「用語集ミニ(同名異義)」も出して。
最後にContext MapをASCII図で描いて、矢印ごとに「ACL/DTOで守るポイント」を箇条書きにして。
次のDTO案を、後方互換を壊さずに進化させたい。
(1) 追加OKなフィールド (2) 破壊変更になりやすい変更 (3) バージョニング戦略(段階的移行)を、
例と一緒に提案して。
ACLの変換コードが肥大化してきた。
(1) どこが設計上の臭いか (2) 分割/整理の案 (3) テスト戦略(変換ルール固定)を提案して。
40-11 よくあるつまずき ここで転ばないでね 🧸💦
-
DTOにドメイン都合を詰めがち → “相手が必要な最小限”だけにする🥗✨
-
共通プロジェクトを作りすぎる → 共通にした瞬間、境界が溶けやすい🫠⚠️
-
「とりあえず参照」で境界が崩壊 → 参照ルールを固定して、違反したらビルドで落とすのが強い🔒💪
-
E2Eが巨大化 → “成功1本+失敗2本”くらいからでOK🎯✨