レイヤー構成
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)