メインコンテンツまでスキップ

レイヤー構成

Bazbiiは3層アーキテクチャを採用しています。 各レイヤーの責務と、なぜこの分離が必要かを説明します。

3層アーキテクチャ

server-apps(アプリケーション層)

役割

アプリケーションのエントリーポイント。外部との通信と、内部レイヤーの配線を担当。

gateway

  • 入力: HTTP/JSON (Connect RPC)
  • 出力: gRPC (API Serverへ)
  • 責務:
    • プロトコル変換
    • JWT認証・認可(入口)
    • レート制限
    • トレース開始
    • エラーフォーマット統一

api

  • 入力: gRPC
  • 出力: gRPC
  • 責務:
    • gRPCハンドラー実装
    • ドメインサービス呼び出し
    • server-platformの実装をserver-coreへ注入

依存関係

  • ✅ server-core: ドメインサービスとポートを使用
  • ✅ server-platform: 実装を取得して注入
  • ❌ 直接的な外部依存(DB、ロガーなど)は持たない

server-core(ドメイン層)

役割

ビジネスロジックとドメインモデルの定義。技術的な詳細は一切含まない。

構造例: post パッケージ

server-core/post/
├── entity.go # Postエンティティ
├── service.go # ユースケース(CreateUserPostなど)
├── ports.go # インターフェース(PostWriterなど)
└── events.go # ドメインイベント(post.createdなど)

依存関係

  • ✅ server-core内の他のパッケージ(sharedなど)
  • ❌ server-platform: 実装に依存しない
  • ❌ server-apps: アプリケーション層に依存しない
  • ❌ 外部ライブラリ(DBドライバ、HTTPクライアントなど)

インターフェースの例

// server-core/post/ports.go
type PostWriter interface {
Insert(ctx context.Context, post Post) (PostID, error)
}

// server-platform/datastore/postgres が実装

server-platform(インフラストラクチャ層)

役割

server-coreで定義されたポート(インターフェース)の実装。

主要パッケージ

datastore/postgres

  • PostRepository, ActorRepository などの実装
  • SQLクエリ実行
  • トランザクション管理

observability

  • Logger, Tracer, Metrics の実装
  • OpenTelemetry統合
  • ログ・トレース・メトリクス送信

security/auth

  • JWT発行・検証実装
  • TokenIssuer, TokenVerifier の実装

依存関係

  • ✅ server-core: ポートを実装するため参照
  • ❌ server-apps: アプリケーション層に依存しない

なぜこの分離が必要か?

1. テスタビリティ

// テスト時にモックを注入可能
service := post.NewService(mockPostWriter)

2. 変更への耐性

  • DBを変更したい → server-platform/datastore だけ変更
  • 認証方式を変更したい → server-platform/security だけ変更
  • ビジネスロジックは変更不要

3. 開発の並行性

  • ドメインロジック開発者とインフラ実装者が並行作業可能
  • インターフェースが契約として機能

4. 明確な責務

  • 「これはビジネスロジックか?技術的詳細か?」の判断が明確
  • レビュー時の境界が明確

実装例:投稿作成

server-core/post/service.go

func (s *Service) CreateUserPost(ctx context.Context, in CreateUserPostInput) (PostID, error) {
// ビジネスロジック(インターフェース経由)
p, err := NewPostDraft(in.H3Index, in.Text, in.ActorID)
if err != nil {
return PostID{}, err
}

postID, err := s.repo.Insert(ctx, p) // インターフェース呼び出し

// ドメインイベント発行(インターフェース経由)
publisher := EventPublisherFromContext(ctx)
publisher.Publish(ctx, NewPostCreatedEvent(postID, in.ActorID))

return postID, nil
}

server-platform/datastore/postgres/post_repository.go

// PostWriterインターフェースの実装
func (r *PostRepository) Insert(ctx context.Context, post post.Post) (post.PostID, error) {
// SQL実装
query := `INSERT INTO posts (id, actor_id, h3index_r9, ...) VALUES ($1, $2, $3, ...)`
_, err := r.db.ExecContext(ctx, query, ...)
return post.ID, err
}

server-apps/gateway/handlers/post_handler.go

// 依存注入のコンポジション
repo := datastore.NewPostRepository(db)
service := post.NewService(repo)
handler := NewPostHandler(service)

関連ドキュメント