第39章:境界越えのデータ設計(DTOとバージョンの超入門)📦📨
0. 今日のゴール🎯✨
境界(Bounded Context)をまたぐときに、
- ドメイン型を持ち出さずに(=境界を壊さずに)🙅♀️🔒
- DTO(契約用データ)を薄く作って🥗
- 壊さずに進化させる(後方互換)🚧➡️✅
これができるようになります💪💕
1. 「境界越え」ってどこで起きる?🌉
たとえばミニECだと、こんな“橋渡し”が発生します👇
- 受注管理BC → 請求BC:請求に必要な注文情報を渡す💳
- 受注管理BC → 配送BC:配送に必要な宛先・荷物情報を渡す🚚
- 在庫BC → 受注管理BC:在庫引当結果を返す📦
ここで大事なのは…
✅ 境界を越えるときは「契約(Contract)」で話す📜✨
契約=DTO / API / イベントみたいな、外に公開してよい形のこと。
第38章でやった「public/internal」も、ここで効いてきます🔒
2. DTOって何?(超ざっくり)📨

DTO(Data Transfer Object)は、
**境界を越えて渡すための“荷物”**🎁
です😊
- ドメイン型(Entity/VO)=中身がルールだらけの“本体”🧠
- DTO=必要最低限だけを詰めた“配送箱”📦
🚨 ありがちな事故:ドメイン型をそのまま外に出す
- 変更が全部“破壊変更”になりやすい💥
- うっかり内部ルールや内部構造を公開してしまう😇
- 依存関係が逆流しやすい(境界が崩れる)🌊🧱
3. DTOを「薄く」するコツ🥗✨(ここ超重要!)

コツA:1ユースケース=1 DTO に寄せる🎯
「なんでも入ったDTO(万能DTO)」はだいたい破滅します💣
- 注文一覧に必要なDTO
- 注文詳細に必要なDTO
- 請求計算に必要なDTO
みたいに分けるのが安全🙆♀️
コツB:相手が必要なものだけ渡す📦➡️📦
境界を越えるデータは、“相手の用語”に合わせて最小限に。
- ✅ 渡す:OrderId, Amount, Currency, OrderedAt
- ❌ 渡さない:受注BCの内部状態機械、割引計算の中間値、内部テーブルID…😵💫
コツC:DTOに「意味の強い型」を詰めすぎない🧱
境界の外では、相手の事情が違います👀
- DTOは プリミティブ寄り(string, number, datetime, id)にしておくと壊れにくいです👌
- “完全なドメイン表現”は、境界の内側だけで守る🛡️
コツD:DTOは「契約」なので、名前は慎重に🏷️✨
OrderDtoみたいな雑名は危険⚠️BillingOrderSummaryとかShippingAddressSnapshotみたいに、用途が分かる名前が安心😊
4. 後方互換って何?(壊さず進化)🕊️✨
✅ 後方互換(Backward Compatible)
古いクライアントが、そのまま動く状態を守ること💕
例:
- サーバーがレスポンスに項目を「追加」しても、古いクライアントが無視して動けばOK🙆♀️
- JSONの“知らない項目”は基本無視されるので、追加は比較的安全になりやすいです✅
(.NET の
System.Text.Jsonも、既定ではPOCOに無いプロパティは無視されます。)(Microsoft Learn)
5. “安全な変更” と “危険な変更” 🟢🔴
🟢 比較的安全(まずはここで進化しよう)✨
- フィールドを 追加(しかも“任意”)➕
- 新しいエンドポイントを追加🆕
- 新しいイベント種類を追加📣
🔴 だいたい破壊変更(バージョン戦略が必要)💥
- フィールド削除🗑️
- リネーム(名前変更)✏️
- 型変更(string→number など)🔁
- 任意→必須(required化)🚨
- 意味変更(同じフィールド名なのに計算ルールが変わる等)🧨
「破壊変更を避けるのが基本で、必要なら明示的にバージョンを上げる」…が鉄板です💡(Microsoft Learn)
6. C#でDTOを作ってみよう(例)💻✨
ここでは「受注管理BC → 請求BC」に渡す“注文サマリ”を想定します📦💳
6.1 v1(最初の契約)📜
public sealed record BillingOrderSummaryDto
{
public Guid OrderId { get; init; }
public decimal TotalAmount { get; init; }
public string Currency { get; init; } = "JPY";
public string Status { get; init; } = "Unknown";
public DateTimeOffset OrderedAt { get; init; }
}
ポイント📌✨
record+init:DTOは“基本イミュータブル寄り”が扱いやすいです😊Statusは string:将来ステータスが増えても壊れにくい(ただし運用ルールは決めよう)📝
6.2 v1.1(安全な進化:任意フィールドを追加)➕🌱
クーポンコードを追加したくなった!🎟️
public sealed record BillingOrderSummaryDto
{
public Guid OrderId { get; init; }
public decimal TotalAmount { get; init; }
public string Currency { get; init; } = "JPY";
public string Status { get; init; } = "Unknown";
public DateTimeOffset OrderedAt { get; init; }
// v1.1: 追加(任意)
public string? CouponCode { get; init; }
}
- 古いクライアント:
CouponCodeを知らない → 無視して動く✅ - 新しいクライアント:
CouponCodeがあれば使える🎉
この「追加で進化」がいちばん平和です🕊️💕
7. System.Text.Jsonで“互換性の考え方”を押さえる🧩✨
7.1 既定動作:知らないプロパティは無視🙆♀️
.NET の System.Text.Json は、既定で POCOに無いプロパティを無視します。(Microsoft Learn)
これが「追加が安全になりやすい」大きな理由です😊
7.2 逆に“厳格モード”もある(境界内だけで使うのが無難)🔍
.NET 8以降は、未知プロパティを拒否して例外にできる設定もあります。(Microsoft Learn)
ただし注意⚠️
- 外部公開APIや、別BCとの連携でこれをやると「追加が即破壊」になりやすいです💥
- “契約をガチガチに固定したい内部”でだけ使う、が安全😊
8. バージョニング戦略(超入門)🧭✨
「追加で済まない破壊変更」をしたいとき、どうする?😵💫 ここで API Versioning の考え方が出てきます📌
8.1 よくあるバージョンの持たせ方📦
代表例はこんな感じ👇
- URLに入れる
/api/v2/orders/... - クエリで持つ
/api/orders?api-version=2 - ヘッダーで持つ
api-version: 2
Azureの設計ベストプラクティスでも、ヘッダーでバージョンを指定する例が紹介されています📚(Microsoft Learn)
8.2 ASP.NET CoreのAPIバージョニング(定番パッケージ)📦🧰
ASP.NET Coreでやるなら、いま主流は Asp.Versioning.* 系です。(NuGet)
(昔の Microsoft.AspNetCore.Mvc.Versioning は置き換えが進んでいます。)(GitHub)
9. “破壊変更”を避ける段取り(段階的に)🚧➡️✅
ステップ1:まず「追加」で逃げられないか考える🧠
- 新フィールド追加で足りない?
- 新エンドポイント追加で足りない?
- 新イベント追加で足りない?
逃げられるなら、それが最強💪💕
ステップ2:リネームしたいなら「二重持ち」して移行する✌️
例:TotalAmount を Amount に変えたい場合
- すぐ消さない🙅♀️
- しばらく両方返す(または両方受ける)
- クライアントが移行したら削除(ここでバージョン上げが必要になりやすい)🆙
ステップ3:どうしても無理なら v2 を作る🆕
- v1は維持(互換)
- v2で新仕様
- v1は非推奨→期限を切って終了(運用が超大事)⏳
10. “DTOのバージョン” と “APIのバージョン” は別物だよ🧠✨
- DTOが v1.1 になった(フィールド追加) → APIのバージョンを上げなくてもいいことが多い🙆♀️
- 破壊変更が必要(削除/型変更/必須化) → APIを v2 にする、が安全✅
この「分けて考える」と混乱が減ります😊
11. ミニ演習🎮✅(手を動かそう!)
演習A:DTOを“薄く”リデザインしてみよう🥗
次のDTOは太りすぎです🐷💦(ありがち)
public class OrderDto
{
public Order DomainOrder { get; set; } = default!; // ←ドメイン丸ごと!🙅♀️
public List<OrderLine> Lines { get; set; } = new();
public string InternalMemo { get; set; } = "";
public string DiscountRuleDebugText { get; set; } = "";
}
✅やること
- 請求BCが必要な最小フィールドだけにして、DTOを作り直してね📦✨
- 「ID」「金額」「通貨」「注文日時」「請求に必要な状態」くらいを目安に😊
演習B:互換性テストを書こう🧪✨
“古いJSON(v1)”を新DTO(v1.1)で読めるか確認!
using System.Text.Json;
using Xunit;
public class DtoCompatibilityTests
{
[Fact]
public void OldJson_V1_ShouldDeserializeInto_V1_1_Dto()
{
var oldJson = """
{
"orderId": "11111111-1111-1111-1111-111111111111",
"totalAmount": 1200,
"currency": "JPY",
"status": "Paid",
"orderedAt": "2026-02-01T10:00:00+09:00"
}
""";
var dto = JsonSerializer.Deserialize<BillingOrderSummaryDto>(oldJson);
Assert.NotNull(dto);
Assert.Null(dto!.CouponCode); // v1には無いのでnullでOK✅
}
}
12. つまずきポイント集(先回り)🧯💕
つまずき①:DTOに required を付けて地雷💣
「必須にしたい!」は分かるけど、後から required を増やすのは破壊変更になりやすいです🚨
まずは 任意で追加 → 移行 → 次のバージョンで必須 の順が安全😊
つまずき②:enumが増えて落ちる😇
- 新しい値が来ても落ちない設計にしておく(Unknownを用意する等)🧩
- 文字列で運ぶなら未知値を受け止めるルールを決めよう📝
つまずき③:金額・日付がBCごとにブレる🌀
- 金額:
amountとcurrencyをセットで💴 - 日付:ISO 8601(
DateTimeOffset)でズレに強く⏰
13. 今日のまとめ🧡
- 境界越えは 契約(DTO) でやる📨
- DTOは 薄く、用途ごと に作る🥗🎯
- 進化はまず 追加(任意) で逃げる➕
- 破壊変更は 段階的移行 or v2 で安全に🚧✅
System.Text.Jsonは既定で未知プロパティを無視しやすく、追加進化と相性がいい🙆♀️(Microsoft Learn)- APIバージョニングは、ヘッダー/クエリ/URLなど戦略がある📦(Microsoft Learn)
- ASP.NET Coreのバージョニング支援は
Asp.Versioning.*が主流📦(NuGet)
お助けAIプロンプト(第39章用)🤖✨
- 「このDTO、太りすぎなら“最小フィールド案”を3つ出して🥗」
- 「この変更は後方互換?破壊変更?理由もつけて判定して⚖️」
- 「破壊変更が必要なときの段階的移行プランを手順で作って🚧」
- 「v1 JSON と v1.1 DTO の互換性テスト(xUnit)を書いて🧪」
- 「APIのバージョン戦略(URL/Query/Header)のメリデメを表にして📊」