カテゴリ: Go言語 更新日: 2026/03/08

Go言語でドメイン駆動設計(DDD)を実現する考え方!初心者でもわかる設計手法完全ガイド

Go言語でドメイン駆動設計(DDD)を実現する考え方
Go言語でドメイン駆動設計(DDD)を実現する考え方

先生と生徒の会話形式で理解しよう

生徒

「先生、ドメイン駆動設計っていう言葉を聞いたんですが、すごく難しそうで...。Go言語でも使えるんですか?」

先生

「ドメイン駆動設計は、略してDDDと呼ばれる設計手法です。ビジネスのルールや概念を中心にプログラムを設計する考え方で、Go言語でも十分に実現できますよ。」

生徒

「ビジネスのルールを中心にって、どういうことですか?普通のプログラミングと何が違うんですか?」

先生

「それでは、ドメイン駆動設計の基本的な考え方から、Go言語でどのように実装するのか、詳しく見ていきましょう!」

1. ドメイン駆動設計(DDD)とは何か?

1. ドメイン駆動設計(DDD)とは何か?
1. ドメイン駆動設計(DDD)とは何か?

ドメイン駆動設計(Domain-Driven Design、略してDDD)とは、エリック・エヴァンスという人が提唱した、ソフトウェア設計の手法です。ドメインとは、アプリケーションが扱う業務領域や問題領域のことを指します。

例えば、ECサイトを作る場合、商品の販売、在庫管理、配送、決済などがドメインになります。銀行のシステムなら、口座管理、入出金、振込などがドメインです。DDDでは、これらのビジネスルールや業務知識を、プログラムの中心に据えて設計します。

従来のプログラミングでは、データベースやWebフレームワークといった技術的な部分から設計を始めることが多くありました。しかし、DDDでは逆に、ビジネスのルールや概念を最初に考え、それを表現するためのコードを書きます。

例えるなら、家を建てるときに、まず住む人の生活スタイルや必要な部屋を考えてから設計するのと似ています。いきなり壁やドアの素材を決めるのではなく、どんな暮らしをしたいかを最初に考えるのです。

2. DDDの主要な概念

2. DDDの主要な概念
2. DDDの主要な概念

ドメイン駆動設計には、いくつかの重要な概念があります。これらを理解することで、DDDの全体像が見えてきます。

エンティティ

一意の識別子を持つオブジェクトです。例えば、ユーザーや注文は、それぞれIDで識別されます。同じ名前のユーザーがいても、IDが違えば別のユーザーです。エンティティは、ライフサイクルを持ち、状態が変化します。

値オブジェクト

識別子を持たず、属性の値だけで判断されるオブジェクトです。例えば、金額や住所などです。1000円という金額は、どの1000円も同じ価値を持ちます。値オブジェクトは、作成後に変更されない(イミュータブル)特性を持ちます。

集約

関連するエンティティや値オブジェクトをまとめたものです。集約には必ずルートとなるエンティティがあり、外部からは必ずこのルートを通してアクセスします。例えば、注文という集約には、注文明細や配送先情報が含まれます。

リポジトリ

集約を保存したり取得したりするための仕組みです。データベースとのやり取りを隠蔽し、ドメインモデルとデータベースを分離します。

ドメインサービス

エンティティや値オブジェクトに属さない、ドメインのロジックを実装する場所です。例えば、送料の計算や在庫の引当処理などです。

3. エンティティの実装例

3. エンティティの実装例
3. エンティティの実装例

それでは、実際のGo言語のコードで、DDDの概念を実装してみましょう。まずはエンティティから見ていきます。ECサイトの注文を例にします。


package domain

import (
    "errors"
    "time"
)

type OrderID int

type OrderStatus string

const (
    OrderStatusPending   OrderStatus = "pending"
    OrderStatusConfirmed OrderStatus = "confirmed"
    OrderStatusShipped   OrderStatus = "shipped"
    OrderStatusDelivered OrderStatus = "delivered"
)

type Order struct {
    id         OrderID
    customerID int
    items      []OrderItem
    totalPrice Money
    status     OrderStatus
    createdAt  time.Time
}

func NewOrder(customerID int, items []OrderItem) (*Order, error) {
    if customerID <= 0 {
        return nil, errors.New("顧客IDが無効です")
    }
    if len(items) == 0 {
        return nil, errors.New("注文商品が空です")
    }
    
    order := &Order{
        customerID: customerID,
        items:      items,
        status:     OrderStatusPending,
        createdAt:  time.Now(),
    }
    
    order.calculateTotal()
    return order, nil
}

func (o *Order) Confirm() error {
    if o.status != OrderStatusPending {
        return errors.New("確定できない状態です")
    }
    o.status = OrderStatusConfirmed
    return nil
}

func (o *Order) calculateTotal() {
    total := 0
    for _, item := range o.items {
        total += item.Price * item.Quantity
    }
    o.totalPrice = Money{Amount: total, Currency: "JPY"}
}

このコードでは、Order構造体がエンティティです。IDを持ち、状態(ステータス)が変化します。フィールドを小文字にすることで、外部から直接変更できないようにしています。状態の変更は、Confirmメソッドなどのビジネスルールをチェックするメソッドを通してのみ行います。これにより、不正な状態変更を防ぎます。

4. 値オブジェクトの実装例

4. 値オブジェクトの実装例
4. 値オブジェクトの実装例

次に、値オブジェクトを実装します。値オブジェクトは、識別子を持たず、値そのものが重要なオブジェクトです。金額を表すMoneyを実装してみましょう。


package domain

import "errors"

type Money struct {
    Amount   int
    Currency string
}

func NewMoney(amount int, currency string) (Money, error) {
    if amount < 0 {
        return Money{}, errors.New("金額は0以上である必要があります")
    }
    if currency == "" {
        return Money{}, errors.New("通貨が指定されていません")
    }
    return Money{Amount: amount, Currency: currency}, nil
}

func (m Money) Add(other Money) (Money, error) {
    if m.Currency != other.Currency {
        return Money{}, errors.New("異なる通貨を加算できません")
    }
    return Money{
        Amount:   m.Amount + other.Amount,
        Currency: m.Currency,
    }, nil
}

func (m Money) Equals(other Money) bool {
    return m.Amount == other.Amount && m.Currency == other.Currency
}

func (m Money) IsGreaterThan(other Money) bool {
    if m.Currency != other.Currency {
        return false
    }
    return m.Amount > other.Amount
}

値オブジェクトの特徴は、変更不可能(イミュータブル)であることです。Addメソッドは、既存のMoneyを変更するのではなく、新しいMoneyを作成して返します。また、Equalsメソッドで、値による等価性判定を実装しています。これにより、1000円という金額は、どの1000円も同じ価値であるというビジネスルールを表現できます。

5. リポジトリの実装例

5. リポジトリの実装例
5. リポジトリの実装例

リポジトリは、集約の永続化を担当します。DDDでは、リポジトリはインターフェースとして定義し、具体的な実装は別の層に配置します。


package domain

type OrderRepository interface {
    Save(order *Order) error
    FindByID(id OrderID) (*Order, error)
    FindByCustomerID(customerID int) ([]*Order, error)
    Delete(id OrderID) error
}

このインターフェースをドメイン層に配置することで、ドメインモデルがデータベースの実装に依存しないようにします。実際のデータベースアクセスは、インフラストラクチャ層で実装します。


package infrastructure

import (
    "database/sql"
    "myapp/domain"
)

type MySQLOrderRepository struct {
    db *sql.DB
}

func NewMySQLOrderRepository(db *sql.DB) domain.OrderRepository {
    return &MySQLOrderRepository{db: db}
}

func (r *MySQLOrderRepository) Save(order *domain.Order) error {
    query := `INSERT INTO orders (customer_id, total_price, status, created_at) 
              VALUES (?, ?, ?, ?)`
    
    result, err := r.db.Exec(query,
        order.CustomerID(),
        order.TotalPrice().Amount,
        order.Status(),
        order.CreatedAt(),
    )
    
    if err != nil {
        return err
    }
    
    id, err := result.LastInsertId()
    if err != nil {
        return err
    }
    
    order.SetID(domain.OrderID(id))
    return nil
}

func (r *MySQLOrderRepository) FindByID(id domain.OrderID) (*domain.Order, error) {
    query := `SELECT customer_id, total_price, status, created_at 
              FROM orders WHERE id = ?`
    
    var customerID int
    var totalPrice int
    var status string
    var createdAt time.Time
    
    err := r.db.QueryRow(query, id).Scan(
        &customerID,
        &totalPrice,
        &status,
        &createdAt,
    )
    
    if err != nil {
        return nil, err
    }
    
    return domain.ReconstructOrder(
        id,
        customerID,
        totalPrice,
        domain.OrderStatus(status),
        createdAt,
    )
}

このように、リポジトリの実装をドメイン層から分離することで、データベースをMySQLからPostgreSQLに変更しても、ドメインモデルには影響を与えません。DDDの重要な原則の一つである、ドメインモデルの独立性が保たれます。

6. ドメインサービスの実装例

6. ドメインサービスの実装例
6. ドメインサービスの実装例

ドメインサービスは、複数のエンティティや値オブジェクトにまたがるビジネスロジックを実装します。例えば、在庫の確認と引当処理を実装してみましょう。


package domain

type StockService struct {
    stockRepo StockRepository
}

func NewStockService(stockRepo StockRepository) *StockService {
    return &StockService{stockRepo: stockRepo}
}

func (s *StockService) AllocateStock(order *Order) error {
    for _, item := range order.Items() {
        stock, err := s.stockRepo.FindByProductID(item.ProductID)
        if err != nil {
            return err
        }
        
        if stock.Quantity < item.Quantity {
            return errors.New("在庫が不足しています")
        }
        
        if err := stock.Allocate(item.Quantity); err != nil {
            return err
        }
        
        if err := s.stockRepo.Save(stock); err != nil {
            return err
        }
    }
    
    return nil
}

func (s *StockService) CalculateShippingFee(order *Order, destination Address) (Money, error) {
    totalWeight := 0
    for _, item := range order.Items() {
        totalWeight += item.Weight * item.Quantity
    }
    
    distance := s.calculateDistance(order.WarehouseAddress(), destination)
    
    baseFee := 500
    weightFee := totalWeight * 10
    distanceFee := distance * 5
    
    total := baseFee + weightFee + distanceFee
    return NewMoney(total, "JPY")
}

ドメインサービスは、特定のエンティティに属さないロジックを実装します。在庫の引当処理は、注文と在庫の両方に関わるため、どちらのエンティティにも属しません。こうした処理は、ドメインサービスとして独立させることで、コードの責任を明確にできます。

7. DDDのレイヤー構成

7. DDDのレイヤー構成
7. DDDのレイヤー構成

ドメイン駆動設計では、アプリケーションを複数の層に分けて構成します。Go言語でDDDを実装する際の、推奨されるディレクトリ構成を見てみましょう。


myapp/
├── domain/
│   ├── model/
│   │   ├── order.go
│   │   ├── product.go
│   │   └── customer.go
│   ├── repository/
│   │   ├── order_repository.go
│   │   └── product_repository.go
│   └── service/
│       ├── stock_service.go
│       └── pricing_service.go
├── application/
│   └── usecase/
│       ├── create_order.go
│       └── confirm_order.go
├── infrastructure/
│   ├── persistence/
│   │   ├── mysql_order_repository.go
│   │   └── mysql_product_repository.go
│   └── web/
│       └── handler/
│           └── order_handler.go
└── go.mod

ドメイン層には、ビジネスルールの中核となるモデルとリポジトリのインターフェースを配置します。アプリケーション層には、ユースケースを実装します。インフラストラクチャ層には、データベースやWebなどの技術的な実装を配置します。この構成により、ビジネスロジックと技術的な詳細が分離され、変更に強いシステムが構築できます。

8. ユビキタス言語の重要性

8. ユビキタス言語の重要性
8. ユビキタス言語の重要性

DDDでは、ユビキタス言語という概念が非常に重要です。ユビキタス言語とは、開発者とビジネス側の人が共通して使う言葉のことです。

共通言語を使う

コード内の変数名、関数名、クラス名は、ビジネス側の人が使う言葉と同じにします。例えば、注文を表すならOrder、在庫ならStockというように、誰が見ても分かる言葉を使います。

ドキュメントとコードの一致

仕様書に書かれている用語と、コードで使われている用語が同じであることが重要です。これにより、仕様書とコードの乖離を防ぎ、コミュニケーションが円滑になります。

会話で使う言葉

開発者同士、また開発者とビジネス側の人が会話するときも、同じ言葉を使います。プログラマーだけが分かる専門用語ではなく、誰もが理解できる業務用語を使うことで、認識の齟齬を減らせます。

9. Go言語でDDDを実践するメリット

9. Go言語でDDDを実践するメリット
9. Go言語でDDDを実践するメリット

Go言語は、ドメイン駆動設計を実践するのに適した言語です。その理由をいくつか見ていきましょう。

シンプルな構文

Go言語のシンプルな構文は、ビジネスロジックを明確に表現できます。複雑な継承よりも、構造体の組み合わせとインターフェースを使うGo言語の設計思想は、DDDの考え方とよく合います。

インターフェースの活用

Go言語の小さなインターフェースは、DDDのリポジトリやドメインサービスの定義に最適です。実装を隠蔽し、依存関係を逆転させるDDDの原則を、自然に実現できます。

パフォーマンス

Go言語の高速性は、複雑なビジネスロジックを持つシステムでも、優れたパフォーマンスを発揮します。並行処理も扱いやすく、大規模なシステム構築に適しています。

Go言語を基礎からスッキリ学びたい人や、 文法だけでなく「実用的な使い方」まで押さえたい人には、 定番の入門書がこちらです。

基礎からわかるGo言語をAmazonで見る

※ Amazon広告リンク

10. DDDを学ぶ際の注意点

10. DDDを学ぶ際の注意点
10. DDDを学ぶ際の注意点

ドメイン駆動設計は強力な手法ですが、すべてのプロジェクトに適しているわけではありません。初心者が押さえておくべきポイントを見ていきましょう。

小規模プロジェクトには過剰

DDDは、複雑なビジネスルールを持つ中規模から大規模のプロジェクトに適しています。シンプルなCRUDアプリケーションでは、かえって開発が複雑になる可能性があります。

ビジネス知識が必要

DDDを実践するには、対象となる業務への深い理解が必要です。ビジネス側の人と密にコミュニケーションを取り、業務の本質を理解することが重要です。

段階的に導入する

最初から完璧なDDDを目指さず、まずはエンティティと値オブジェクトから始めましょう。プロジェクトの成長に合わせて、徐々にDDDの概念を取り入れていくのが現実的です。

ドメイン駆動設計は、ビジネスロジックを中心に据え、技術的な詳細から独立させることで、変更に強く保守しやすいシステムを構築する手法です。Go言語のシンプルさと、DDDの明確な設計原則は相性が良く、実践的なシステム開発に役立ちます。最初は概念が難しく感じるかもしれませんが、実際のプロジェクトで少しずつ適用していくことで、その価値を実感できるでしょう。

関連セミナーのご案内

【超入門】ゼロから始めるGo言語プログラミング:最速で「動くアプリ」を作るマンツーマン指導

「プログラミングの仕組み」が根本からわかる。Go言語でバックエンド開発の第一歩を。

本講座を受講することで、単なる文法の暗記ではなく、「プログラムがコンピュータの中でどう動いているか」という本質的な理解につながります。シンプルながら強力なGo言語(Golang)を通じて、現代のバックエンドエンジニアに求められる基礎体力を最短距離で身につけます。

具体的な開発内容と環境

【つくるもの】
ターミナル(黒い画面)上で動作する「対話型計算プログラム」や、データを整理して表示する「ミニ・ツール」をゼロから作成します。自分の書いたコードが形になる感動を体験してください。

【開発環境】
プロの現場でシェアNo.1のVisual Studio Code (VS Code)を使用します。インストールから日本語化、Go言語用の拡張機能設定まで、現場基準の環境を一緒に構築します。

この60分で得られる3つの理解

1. 環境構築の完全な理解

「なぜ動くのか」という設定の仕組みを理解し、今後の独学で詰まらない土台を作ります。

2. Go言語の基本構造(変数・型)

データの種類やメモリの概念など、他言語にも通じるプログラミングの本質を学びます。

3. 読みやすいコードの書き方

ただ動くだけでなく、誰が見ても分かりやすい「綺麗なコード」を書くための考え方を伝授します。

※本講座は、将来的にバックエンドエンジニアクラウドインフラに興味がある未経験者のためのエントリー講座です。マンツーマン形式により、あなたの理解度に合わせて進行します。

セミナー画像

初めてのGo言語を一緒に学びましょう!

カテゴリの一覧へ
新着記事
New1
Kotlin
Kotlinの関数リテラル(匿名関数)の作り方と使い方をやさしく解説!初心者向け関数型プログラミング入門
New2
Kotlin
Kotlinのfold・reduceを使った累積計算テクニック
New3
Go言語
Go言語でドメイン駆動設計(DDD)を実現する考え方!初心者でもわかる設計手法完全ガイド
New4
Kotlin
Kotlinのdistinct・toSetで重複を削除する方法
人気記事
No.1
Java&Spring記事人気No1
Kotlin
Kotlinのsettings.gradleファイルを完全解説!初心者でもわかるプロジェクト設定の基本
No.2
Java&Spring記事人気No2
Go言語
Go言語のgo.modファイル完全ガイド!初心者でもわかる仕組みと書き方
No.3
Java&Spring記事人気No3
Swift
Swift Playgroundの使い方を完全解説!初心者に最適な学習環境の始め方
No.4
Java&Spring記事人気No4
Kotlin
Gradleファイル(build.gradle.kts)の書き方と役割をやさしく解説!Kotlin初心者向け完全ガイド
No.5
Java&Spring記事人気No5
Kotlin
Kotlinのログ出力方法を完全ガイド!LogcatとTimberでトラブルシューティング
No.6
Java&Spring記事人気No6
Swift
Swift開発環境の構築方法を徹底解説!Xcode・Windows・Linux対応
No.7
Java&Spring記事人気No7
Kotlin
KotlinのAPI通信でPOSTリクエストを送る方法!初心者向け徹底ガイド
No.8
Java&Spring記事人気No8
Kotlin
Kotlinの演算子一覧と使い方!算術・比較・論理演算子の基本を解説