【GoCon前夜祭登壇レポート】GoによるDDDとTDDを用いたリアーキテクチャの実践
記事を書いた経緯
弊社で Go Conference 2025 の前夜祭として GO!GO!前夜祭 〜OSTで広がるGoの輪〜 が開催されました。
少し期間が空いてしまいましたが、GoでDDD(ドメイン駆動設計)とTDD(テスト駆動開発)を用いてリアーキテクチャを実践している話について登壇した内容について文章でも残しておこうと思います。
※ 筆者はまだTDDとDDDを学びながら試行錯誤している最中なので誤りやアドバイスなどあればコメントしていただけると幸いです。あくまで私たちのチームではこのような事をしている方といった共有として見ていただけると幸いです。
リアーキテクチャの経緯
現行システムの課題

スポットバイトルという弊社のサービスは、スポットバイトで働くワーカー、求人を出すクライアント、弊社の社員であるCS(カスタマーサポート)といった使用者ごとの単位に分けられた複数のチームがそれぞれのシステムを開発しており、共通のDB、テーブル、レコードを参照・更新しており、下記のような状態でした。
- 一つのデータモデルに複数のシステムが依存していて結合度が高い
- ビジネスロジックも同様に各システムに分散していて凝集度が低い
これらの理由から矛盾が起きないように全チームで足並みを揃えて開発を行うことが常態化しており、
開発スピードの低下や、意図しないバグの発生などリリースから期間が経って課題が顕在化してきました。
半年といった急ピッチで開発してリリースしたことによる負債を抱えながらサービスを拡大している状態から脱して早い市場の変化に対応しながら長期間サービスを稼働させる土台を構築するため、
現状のような画面中心の設計から業務中心の設計にリアーキテクチャするためにDDD(ドメイン駆動設計)を選択し、開発速度を高めながらも品質を担保して開発することが出来ることを狙ってTDD(テスト駆動開発)を取り入れてリアーキテクチャを始めていきました。
DDD(ドメイン駆動設計)とは
DDD(ドメイン駆動設計)とはソフトウェアの事業活動の目的を理解し、その事業目的を達成するためにどのような事業活動が行われているかを分析し、その分析結果を活かしてソフトウェアを設計する方法です。
戦略的DDDと戦術的DDD
DDD(ドメイン駆動設計)は大きく戦略的DDDと戦術的DDDのフェーズに分けられます。
戦略的DDD
戦略的DDDは、複雑な業務を全体的かつ高い視点からモデリングするための設計理論。
複雑なビジネスドメインを理解し、整理することでより小さく管理しやすい単位に分割します。
例)
- ユビキタス言語
- 境界づけられたコンテキスト
- コアドメイン、汎用サブドメイン
 etc...
戦術的DDD
戦術的DDDはモデリングされた業務をシステムに落とすための詳細な手法。
戦略的DDDのステップで定義した単位で、具体的なビジネスロジックをコードとして実装します。
例)
- ユビキタス言語を用いたコードの実装
- 値オブジェクト
- エンティティ
- 集約
 etc...
イベントストーミング [戦略的DDD]
AWSさんにサポートしていただき、イベントストーミングを行いました。
イベントストーミングを行ったことで実際の業務を基にした集約、システムの単位となる境界づけられたコンテキスト、コンテキストの依存関係であるコンテキストマップを発見することができました。
各チームのエンジニアやPO(プロダクトオーナー)が一室に集まって行ったことで、自分の知らなかった業務に関して知ることができたり、その場で業務仕様の認識を合わせてユビキタス言語を定義できたりとDDDを始める土台を構築するにあたって非常に有意義な時間となりました。
実際の詳細な取り組みに関しては下記のブログに詳しく書かれているので本ブログでは省略いたしますが、
気になる方は下記のブログも合わせてご覧ください。
Goによるドメインモデルの表現 [戦術的DDD]
戦術的DDDのフェーズではデータを格納している構造体とビジネスロジックが分散していた状態を改善する方法としてDDD(ドメイン駆動設計)の思想を基にドメインモデルを設計した話をしました。
値オブジェクト
Goではプリミティブな値にエイリアスをつけて明示的に別の型(defined type)として定義することができます。
type Money uint64
構造体による値オブジェクトだけではなく、値オブジェクトを使える際には出来るだけ使うために、defined typeを用いて、プリミティブな型で扱っていたデータを業務の言葉である型に置き換えていきました。
これによって下記のようなメリットがある
ロジックのカプセル化
プリミティブな型で表現していた値を値オブジェクトとして扱うことで、値オブジェクトに関連するロジックをその型自身に持たせることができます。
これによって外部から値の操作に関する詳細を知る必要がなくなるため、分散していたロジックをカプセル化し、凝集性を高める事ができます。
ロジックが業務と同じ言葉の型で記述されていく
金額は金額同士でしか計算しないため、引数にはMoney型しか渡す事ができなくさせて型安全性を高める事が可能になります。
func (m Money) Add(other Money) (Money, error) {
    result := m.Value() + other.Value()
    return NewMoney(result)
}
また、業務と同じ型、言葉を用いてロジックを記述するため、構造体のフィールドやロジックを見た際の認知負荷がかなり下がることを実感しました。
また、AIが急激に進化している昨今のメリットとしてはAIともユビキタス言語が共有でき、自然言語を用いて指示や質問を与えた際の精度が向上すると感じました。
弊社ではPO(プロダクトオーナー)も自然にDevinに質問できる環境なので、型レベルでユビキタスを共有できていることは恩恵が高いと感じています。
集約
業務を表し、トランザクションの単位となる集約のルート構造体に紐づくエンティティや値オブジェクトを非公開フィールドとして定義してカプセル化を行いました。
集約に紐づくすべての要素が集約のルートのメソッドを通じてのみ操作されるため、ドメインモデルがビジネスルールに常に守られて整合性が担保されます。
また、外部からは集約の内部の詳細を知らずに命令するだけでよくなります。
type Order struct {
    id          OrderID
    customerID  CustomerID
    items       []*OrderItem
    totalAmount Money        
    status      OrderStatus  
}
func (o *Order) AddItem(price Money) error {
    newItem, err := newOrderItem(price)
    if err != nil {
    	return fmt.Errorf("アイテム作成エラー: %w", err)
    }
    o.items = append(o.items, newItem)
    newTotal, err := o.totalAmount.Add(newItem.GetPrice())
    if err != nil {
    	return fmt.Errorf("合計金額更新エラー: %w", err)
    }
    
    o.totalAmount = newTotal 
    return nil
}
Goの言語仕様による課題
ドメインモデルの設計を進める中でGoの言語仕様による課題がいくつかありました。
このような課題に対してはモデルの制約の厳格化とGo特有の簡潔さはトレードオフになるため、どこまで完全なカプセル化を追求するかといった認識をチームで合意をとって定めて進めていく事が大切だと感じました。
個人的には開発体験を下げるほどの厳格な制約がなくても成立するように根底にあるDDD(ドメイン駆動設計)の思想、メリットをチーム内で共有できる仕組みと共にコーディングルールとして制御するのが理想に感じています。(弊社では「ドメイン駆動設計を始めよう」の輪読会をチーム横断でPO(プロダクトオーナー)含めて始めてみるといった取り組みから始めてみました。)
不変性の担保
Goには、Javaのfinalのようなオブジェクトの不変性を言語レベルで強制する仕組みがありません。
フィールド小文字にしていたとしても同一パッケージ内からは値を変更する事が可能になってしまうため、不変性を完全に担保することはできません。
また、不変性を守るためだけにパッケージを厳密に切っていくのも認知負荷が上がってしまうため、設計判断が必要です。
例)
同一パッケージ内から値を変える事が可能なので不変性の担保は書き手次第
type Money struct {
    amount uint64 // 非公開フィールド(小文字)
}
func NewMoney(amount uint64) Money {
    return Money{amount: amount}
}
// 不変性が担保できていない
func change(m *Money, newValue uint64) {
    m.amount = newValue 
}
コンストラクタの使用を強制できない
Goにはクラスを主とした多言語のようにコンストラクタを強制させる仕組みがありません。そのためコンストラクタにバリデーションロジックなどをカプセル化したとしても、オブジェクトを作成できてしまいます。
業務的に不整合な値が入らないことの担保が完全にはできません。
例)
コンストラクタの使用は書き手・次第
email, err1 := NewEmailAddress("user@example.com")
if err != nil {
  fmt.Printf("無効なメールアドレス: %s\n", err)
}
// 不正な値を防ぐ事ができない
invalidEmail := EmailAddress("hogehoge")
※ 下記のようにインターフェースを構造体に埋め込むことでコンストラクタを矯正することはできますが、Go特有の簡潔に記述する方針とは逸れてしまうため、こちらもチームによるトレードオフの判断が必要になる
DDD(ドメイン駆動設計) まとめ
DDD(ドメイン駆動設計)を用いて開発することで、それまで業務が密に結合して複雑化していたロジックの責務が分割され、それぞれの業務モデルのロジックが簡潔になっていきました。
また、データとロジックがバラバラになってしまったコードをカプセル化することで見通しが良く高凝集なコードへと改善されていくことを共に体感できました。
Goの言語仕様による課題に対しては、完璧な厳格さと開発のしやすさのトレードオフとなると感じたので、チームで認識を合わせて設計判断を行い、そのルールをAIや次新たに参画するメンバーに無理なく伝えていく仕組みを構築する事が非常に大切だと感じました。
また、戦略的フェーズのイベントストーミングに関してはリアーキテクチャに関わらず、業務フローが変わるような機能の改修を行う際には毎回行いたいくらい良い体験だったので一度限りにしないよう開発フローに取り組んでいく動きを進めています。
TDD(テスト駆動開発)とは
TDD(テスト駆動開発)とは先にテストコードを書き、そのテストをパスするための実装コードを書いていくという開発手法です。
このサイクルを繰り返すことで、バグの少ない、保守しやすいコードを効率的に開発できます。
と言われても良さはわかりづらいと思うのでTDDテスト駆動開発)の基本サイクルについて説明した後にGoでどのように行ったか、実践しての学びを共有したいと思います。
私たちはt-wadaさんのライブコーディングをチームで見る時間を取りTDDをキャッチアップしてから実際の開発を進めていきました。
2時間でTDD(テスト駆動開発)を実践する理由や嬉しさを実際にt-wadaさんがコードを書いているところを見ながら学ぶことができるためとてもオススメです。
TDD(テスト駆動開発)の基本サイクル
1. 次の目標(業務仕様、振る舞い)を考える
ここで定めた目標が次のテストケースになります。
ここの適切な粒度は開発者の経験に依存するため、最も難しいポイントかもしれません。小さすぎても開発スピードを損ねてしまいますし、大きすぎてもサイクルの回数が少なくなってしまうのでただテストを先に書いて実装しているだけになってしまいます。
最初に満たすべき要件を整理してテストのリストをtodoリストのように整理してテストの容易性が高い物から手を付けていくと良いと思います。(外部に依存しない処理など)
または、テストリストをテスト用意性が高い形に分解して進めるのもいいと思います。
2. その目標を示すテストを書く
1のフェーズで定めた目標を検証するテストケースを書きます
3. そのテストを実行して失敗させる(Red)
ここのステップで失敗させる事が大切です。
実装していないから失敗するのですが、テストが想定通りの挙動で失敗する事を確認する事が大切です。コードの状態を把握するためにも、そもそも検証するテストケース自体に誤りがないかということに気付くことが出来ます。
4. テストを最短でパスするコードを書く
3のフェーズで書いたテストを最速で満たすべきコードを書くことが大切です。
コードにこだわりたい心を捨ててサイクルを早く多く回すことでコードが改善されていきます。
5. 2で書いたテストを成功させる(Green)
実装が終わったので、ここでテストが成功する事を確認します。
6. リファクタリングを行う
テストが成功してGreenの状態になってからご褒美のリファクタリングタイムです。
テストが成功した状態でリファクタリングを行うことで、振る舞いが変わらない事が担保されます。個人的にはここがTDDの素晴らしいポイントだと感じました。
リファクタリングの対象にはテストも含まれています。
実装を進めていく中で共通化処理を行うことが出来たり、不要になっているテストも出てきます。
7. 1~6のフェーズを繰り返す
上記のサイクルを一つずつテストを増やして繰り返していきます。
サイクルが進むによって実装が複雑になっていくと思いますが、テストケースを積み上げていっているのでデグレなどを検知しながら安全に開発を進めていくことが出来ます。
GoによるTDD(テスト駆動開発)のテスト戦略
私たちはt-wadaさんのライブコーディングをを参考に進めたため、
その始めやすさからテーブル駆動ではなく、最初はt.runを構造化する形でテストケースを一つずつ定義していく単純なAAAパターンを用いてテストを記述しました。
func TestAdd(t *testing.T) {
    t.Run("正の数同士の足し算ができる。", func(t *testing.T) {
     // AAA パターン
     // 1. Arrange (準備): テストに必要な初期状態や入力データを設定
     a := 10
     b := 5
     expected := 15
     // 2. Act (実行): テスト対象の関数を実行
     actual := add(a, b)
     // 3. Assert (検証): 実行結果と期待値を比較
     if actual != expected {
        t.Errorf("add(%d, %d) returned %d, want %d", a, b, actual, expected)
    }
    })
この手法はテーブル駆動のテストとは異なり完全にテストケースが独立します。
(ここではAAAパターンの各フェーズの方法を完全に他のテストに影響を受けずにテストを書くことが可能という形で独立という言葉を用いています。)
このように他のテストに依存しない形で実装できるため、サイクルが進むにあたって変化、詳細化していくテスト観点にも対応しやすい事からへのキャッチアップがしやすい手法だという事がGoにおいても実践できました。
そのため、下記のような使い分けでTDD(テスト駆動開発)実践していきました。
開発時のサイクルには単純なt.runを用いたAAAパターンで実装。
最終的にAIにテストをリファクタリングさせテーブル駆動テストに変換すべきところは変換。
また、境界値などの品質をあげるためのテストは後からこの段階でテストケースを追加して実装。
TDD(テスト駆動開発) まとめ
実践してみて感じたメリットですが、TDD(テスト駆動開発)はAIと実装を進めていくにあたってすごく相性の良い開発手法だと感じました。
期待する振る舞いであるテストケースの単位で開発を進める性質上、
テストコードそのものがプロンプトになる事、小さなステップ単位に分解して支持を出す事といった理由から、AIが適切にこちらの支持をくみ取ってくれることを実感できました。
また、生成コードをその場で検証可能できるため、安全性が担保されると共に
テストをまず実行できることでレビューの負担も減少できると感じました。
また、次の目標(業務仕様、振る舞い)を考えるフェーズの難しさですが、AIの進歩によって無理のない範囲でその単位を大きくして開発スピードを確実に上げられることを実感できました。
また、
今まで実装を書いてから、その後にそれに基づいてテストを書いていましたが、テストを書く意味を深く考えずにカバレッジを満たすためにテストを書いてしまっていたと反省しました。
そのため、テストが実装に依存するといった本末転倒な事が起こってしまっていました、、
TDDを用いることで満たすべき振る舞いを基に本質的なテストを書きつつ、カバレッジも自然と満たされれるといった理想的なサイクルを積む事ができるようになったと実感しています。
DDD(ドメイン駆動設計)とTDD(テスト駆動開発)のシナジー
DDD(ドメイン駆動設計)とTDD(テスト駆動開発)の組み合わせによってテスト単位が満たすべき業務の振る舞い単位で作成できるため、本質なテストケースを設定しやすい。
テストケースが業務の言葉で構造化されるので、テストが動く仕様書になります。
これによって、極論コードの仕様を表すようなドキュメントを書く必要がなくなると考えています。
更新されずに放置されるドキュメントやドキュメントを整備する時間を本質的な価値を生み出す時間に使う事がこれらの開発手法を洗練させたチームになれば、今後実現することが出来ると感じています。
(見返したらスライド確認不足でテストケースで英語の変数名とかゼロ値とかいう言葉を使ってしまっていました。すみません。)
また、DDD(ドメイン駆動設計)を用いる事でそれぞれの責務が単一になるため、ロジックが自然と簡潔になっていくことを感じました。これもTDD(テスト駆動開発)のしやすさ、AIの活用しやすさを高めていると思います。
終わりに
自分の学びを振り替える意味でも書いていたら長文になっていまい、申し訳ないです。
今後は、新規で書いていくリアーキテクチャのコードで引き続きGoによるDDD(ドメイン駆動設計)とTDD(テスト駆動開発)を実践して知見を高めていくと共に、それらを既存のコードに少しずつ取り入れて改善していく方法を試行錯誤していきたいと思います。
また、自分は外部での登壇は初めてだったのでとても良い経験になりました。
他の会社のエンジニアの方や学生エンジニアの方々と技術について話すのは学びだけでなく、刺激をもらって更にモチベーションが上がるきっかけとなりました。
スライドにこだわる時間がとれなかったや、資格試験なども重なってブログの更新が登壇からかなり期間が空いてしまったことが反省点ではありますが、、
このようにディップ株式会社ではエンジニアのイベントやっていくのでぜひ、チェックしてみてください!




Discussion