メインコンテンツへスキップ
  1. Posts/

Goでモジュラーモノリスを設計する:構成、境界、実践パターン

·173 文字·1 分·
Go Architecture Golang Modular-Monolith Architecture
アミット・デイブ
著者
アミット・デイブ
ソフトウェアエンジニアで、しっかりしたシステムを作るのが得意です。難しい問題をわかりやすくすることも得意です。好きなことは、分散システムクリーンアーキテクチャ、そしてオープンソースプロジェクトです。仕事以外では、山を歩くことや日本語の勉強、そしてstderrなどから学ぶことを楽しんでいます。
目次
Go Architecture - この記事は連載の一部です
パート : この記事

断り書き:俺はN5レベルで日本語を勉強中です。間違いがあるかもしれないけど、頑張っています!

マイクロサービスの話で、大事な質問がよく飛ばされる:本当にどんな問題を解決したいのか?ほとんどのチーム — 特にスタートアップや20人以下のエンジニアチーム — にとって、マイクロサービスの運用コストは、解決する問題より多くの問題を作る。モジュラーモノリスはたいてい正しいデフォルトだ:一つのデプロイ、強い内部の境界、簡単に進化できる。

「モジュラー」の本当の意味
#

モジュールは自分のドメインを持って、小さい公開インターフェースを定義して、内部を外に見せない。

実際にはこういうことだ:

  • 各モジュールは自分の型、データベースアクセス、ビジネスロジックを持つ
  • 他のモジュールは公開インターフェースだけを使う
  • ワイヤリングレイヤー(internal/app)が全部を組み立てる — モジュール同士は知らない

モジュールの実装を変えて、インターフェースの外を何も変えなくていいなら、境界は正しく引けている。

ディレクトリの構成
#

cmd/service/
  main.go              ← 依存を作って、サーバーを開始
internal/
  app/                 ← 全部のワイヤリングを持つ場所
  orders/
    service.go         ← 公開インターフェースと型
    repository.go
    model.go
  billing/
    service.go
    repository.go
  users/
    service.go
    repository.go
pkg/                   ← 共有ヘルパー(ロギング、リトライ、ページネーション)
api/                   ← protobuf / OpenAPI の定義
migrations/
go.mod

internal/app は組み立ての場所だ — 全部のモジュールを作って、依存を注入する。main.goapp.New() を呼んでサーバーを開始する。app の外は具体的な実装を知らない。

モジュールの組み立て
#

graph TD
  cmd["cmd/service"] -->|starts| app["internal/app"]
  app -->|injects| orders["orders"]
  app -->|injects| users["users"]
  app -->|injects| billing["billing"]
  orders -->|"OrderCreated event"| billing

app は全部のモジュールの参照を持っている。コードベースで具体的な型を見る唯一の場所だ。全部のモジュールは依存をインターフェースとして受け取る。

うまくいく3つのパターン
#

1. インターフェース駆動の境界
#

各モジュールは小さいインターフェースを公開する:

// internal/orders/service.go
type Service interface {
    Create(ctx context.Context, o Order) (Order, error)
    Get(ctx context.Context, id string) (Order, error)
}

internal/app はこのインターフェースを billing に注入する — *orders.ServiceImpl じゃない。モジュールが独立してテストできて、別々に進化できる。

2. プロセス内イベントで非同期の流れ
#

全部が直接の呼び出しである必要はない。注文が作られた時、billingは反応する必要がある。でも、billingが注文の作成をブロックするべきじゃない。

シンプルなプロセス内イベントバスを使う:

// orders は成功した Create の後にパブリッシュする
bus.Publish(ctx, "orders.created", OrderCreatedEvent{OrderID: o.ID, Amount: o.Total})

// billing は起動時にサブスクライブする
bus.Subscribe("orders.created", billing.HandleOrderCreated)

注文の作成は速いままで、billingの動作は独立してテストできる。耐久性が必要になったら、プロセス内バスを本物のキューに変える — モジュールのコードは変わらない。

3. 境界での腐敗防止
#

あるモジュールが他のモジュールのデータを必要とする時、境界で変換する。orders.Orderbilling に渡さない — billing.OrderSummary を定義して、明確に変換する。内部の表現がモジュールの外に漏れるのを防いで、将来のリファクタリングを楽にする。

テスト戦略
#

  • ユニットテスト:各モジュールを独立して、モックしたインターフェースでテスト。速くて、I/Oなし。
  • 統合テスト:本物の app をテスト用データベースで起動して、全体をテストする。
  • テストヘルパーをモジュール間で共有しない。少しの重複の方が、テストコードでのモジュール間結合よりずっとマシだ。

いつサービスに分けるか
#

本当に必要な時に分ける:

  • 独立したスケーリング — このモジュールは他の10倍のトラフィックがあって、それを計測した
  • 独立したデプロイ — コンプライアンスや規制で分離が必要
  • 違う技術 — モノリスでは満たせない制約がある

「いつかスケールが必要になるかも」は理由じゃない。運用の複雑さは本当のコストだ — 他の方法がもっと悪い時だけ払う。

おわりに
#

モジュラーモノリスはマイクロサービスへの妥協じゃない — 運用の複雑さを低く保ちながら、後でリファクタリングして分割できる意図的なアーキテクチャの選択だ。Goでは、internal/、小さいインターフェース、プロセス内イベントバス、薄い組み立てレイヤーがあれば、チームの成長に耐えるシステムを作れる。

ここから始めよう。サービスに分けるのは、本当に必要になってからだ。

Go Architecture - この記事は連載の一部です
パート : この記事

関連記事

Goプロジェクトの構成方法(実用的な慣習、ルールじゃない)
·147 文字·1 分
Go Architecture Golang Project-Structure Monolith Best-Practices
配管地獄を止める:`bs` を作っている話
·80 文字·1 分
Go Devops Build-Systems Bs Engineering