実装パターン
バックエンドでよく使うパターンとベストプラクティスを説明します。
依存注入パターン
コンストラクタインジェクション
// server-core/post/service.go
type Service struct {
repo PostWriter
}
func NewService(repo PostWriter) *Service {
return &Service{repo: repo}
}
Context経由の依存注入
// イベント発行などの横断的関心事
publisher := EventPublisherFromContext(ctx)
publisher.Publish(ctx, event)
Repository パターン
インターフェース定義(core)
// server-core/post/ports.go
type PostWriter interface {
Insert(ctx context.Context, post Post) (PostID, error)
}
実装(platform)
// server-platform/datastore/postgres/post_repository.go
type PostRepository struct {
db *sql.DB
}
func (r *PostRepository) Insert(ctx context.Context, post post.Post) (post.PostID, error) {
// SQL実装
}
ドメインイベントパターン
イベント定義
// server-core/post/events.go
const EventPostCreated = "post.created"
func NewPostCreatedEvent(postID PostID, actorID ActorID) DomainEvent {
return NewEvent(EventPostCreated, map[string]any{
"post_id": postID.String(),
"actor_id": actorID.String(),
})
}
イベント発行
// サービス内で発行
publisher := EventPublisherFromContext(ctx)
publisher.Publish(ctx, NewPostCreatedEvent(postID, actorID))
Context の活用
ActorIDの伝播
// Gatewayで設定
ctx = ctxutil.WithActorID(ctx, actorID)
// API Serverで取得
actorID, ok := ctxutil.GetActorID(ctx)
トレースIDの伝播
// OpenTelemetryが自動的に伝播
// Traceparent ヘッダが自動付与
エラーハンドリング
ドメインエラー
// server-core/post/service.go
if err := validatePost(text); err != nil {
return PostID{}, fmt.Errorf("invalid post: %w", err)
}
インフラエラー
// platform実装でラップ
if err != nil {
return fmt.Errorf("failed to insert post: %w", err)
}
トランザクション管理
サービス層でのトランザクション
// トランザクションはplatform層で管理
// 必要に応じてトランザクションコンテキストを注入
テストパターン
モックの使用
// テストでモックRepositoryを注入
mockRepo := &MockPostWriter{}
service := post.NewService(mockRepo)
テーブル駆動テスト
tests := []struct {
name string
input CreateUserPostInput
wantErr bool
}{
{"正常系", CreateUserPostInput{...}, false},
{"テキスト長超過", CreateUserPostInput{...}, true},
}