ADR-12: APIHandlers Composition Pattern
| Date | Author | Repos |
|---|---|---|
| 2025-01-03 | @KubrickCode | web |
Context
The Single Interface Constraint
oapi-codegen's strict-server mode generates a single StrictServerInterface that must be implemented by one struct. This creates a tension with the Feature-Based Module Organization (ADR-11), where each domain module (analyzer, auth, user, github, etc.) maintains its own handler implementation within its Clean Architecture layers.
The Problem:
StrictServerInterfacedefines all API endpoints in one interface (~20+ methods)- Feature-based modules have separate handler packages (
modules/analyzer/handler/,modules/auth/handler/, etc.) - Each module's handler only knows about its own domain logic
- The server requires a single struct implementing all interface methods
Without Composition:
// Impossible: Each handler only implements a subset of methods
var _ StrictServerInterface = (*AnalyzerHandler)(nil) // Missing auth methods
var _ StrictServerInterface = (*AuthHandler)(nil) // Missing analyzer methodsThe initial implementation used a single handler struct with all methods. As the codebase grew and adopted Feature-Based Module Organization, a composition pattern became necessary to maintain domain separation while satisfying the single interface requirement.
Decision
Adopt the APIHandlers Composition Pattern to combine multiple domain handlers into a single StrictServerInterface implementation.
Pattern Structure
// Domain-specific handler interfaces (handlers.go)
type AnalyzerHandlers interface {
AnalyzeRepository(ctx context.Context, request AnalyzeRepositoryRequestObject) (AnalyzeRepositoryResponseObject, error)
GetAnalysisStatus(ctx context.Context, request GetAnalysisStatusRequestObject) (GetAnalysisStatusResponseObject, error)
}
type AuthHandlers interface {
AuthCallback(ctx context.Context, request AuthCallbackRequestObject) (AuthCallbackResponseObject, error)
AuthLogin(ctx context.Context, request AuthLoginRequestObject) (AuthLoginResponseObject, error)
// ...
}
// Composite struct implementing full interface
type APIHandlers struct {
analyzer AnalyzerHandlers
auth AuthHandlers
bookmark BookmarkHandlers
// ...
}
var _ StrictServerInterface = (*APIHandlers)(nil) // Compile-time check
// Delegation to domain handlers
func (h *APIHandlers) AnalyzeRepository(ctx context.Context, request AnalyzeRepositoryRequestObject) (AnalyzeRepositoryResponseObject, error) {
return h.analyzer.AnalyzeRepository(ctx, request)
}Key Principles
- Domain-Specific Interfaces: Each handler interface contains only methods relevant to its domain
- Single Composite Struct:
APIHandlersaggregates all domain handlers - Delegation Pattern: Each method delegates to the appropriate domain handler
- Compile-Time Verification:
var _ StrictServerInterface = (*APIHandlers)(nil)ensures completeness
Options Considered
Option A: APIHandlers Composition Pattern (Selected)
How It Works:
- Define domain-specific handler interfaces matching StrictServerInterface method subsets
- Create composite
APIHandlersstruct holding all domain handlers - Implement StrictServerInterface by delegating to appropriate handlers
- Wire everything in
app.gousing dependency injection
Pros:
- Domain Isolation: Each handler only knows about its own domain
- Independent Testing: Domain handlers testable without other dependencies
- Clear Ownership: Each module owns its handler implementation
- Compile-Time Safety: Missing implementations caught at build time
- Extensibility: Adding new domains requires only new interface + handler
Cons:
- Additional boilerplate for delegation methods
- One more layer of indirection
- Interface definitions must stay synchronized with OpenAPI spec
Option B: Single Monolithic Handler
How It Works:
- One large handler struct implementing all StrictServerInterface methods
- All UseCase dependencies injected into single struct
- Direct method implementations without delegation
Pros:
- Simpler structure with no delegation layer
- Fewer files to maintain
- Direct method implementations
Cons:
- Violates Single Responsibility: One struct handles all domains
- Testing Complexity: Requires mocking all dependencies for any test
- Scalability Issues: File grows unbounded as API expands
- Poor Cohesion: Unrelated business logic mixed in one file
- Conflicts with Feature-Based Organization: Undermines module boundaries
Option C: Runtime Router Dispatch
How It Works:
- Register handlers dynamically by path prefix
- Router dispatches to appropriate handler at runtime
- Each handler implements partial interface
Pros:
- Maximum flexibility for handler registration
- No interface synchronization needed
Cons:
- No Compile-Time Safety: Missing handlers discovered only at runtime
- Complex Registration Logic: Error-prone handler wiring
- Defeats StrictServerInterface Purpose: Loses type safety benefits
- Debugging Difficulty: Dispatch errors hard to trace
Option D: Code Generation for Composition
How It Works:
- Generate composition layer from handler interfaces
- Auto-generate delegation methods based on interface definitions
Evaluation:
- Additional tooling complexity
- Custom code generation maintenance burden
- Pattern is simple enough that manual implementation is acceptable
- Rejected: Overhead not justified for current scale
Implementation
Handler Interface Definition
Domain handler interfaces are defined in internal/api/handlers.go, adjacent to the generated server.gen.go:
internal/api/
├── handlers.go # Domain handler interfaces + APIHandlers composite
└── server.gen.go # Generated StrictServerInterfaceWiring in Application
In common/server/app.go, handlers are created and composed:
func initHandlers(container *infra.Container) (*Handlers, error) {
// Create domain handlers
analyzerHandler := analyzerhandler.NewHandler(...)
authHandler := authhandler.NewHandler(...)
userHandler := userhandler.NewHandler(...)
githubHandler := githubhandler.NewHandler(...)
// Compose into single interface
apiHandlers := api.NewAPIHandlers(
analyzerHandler,
userHandler, // AnalysisHistoryHandlers
authHandler,
userHandler, // BookmarkHandlers
githubHandler,
githubAppHandler,
analyzerHandler, // RepositoryHandlers
webhookHandler,
)
return &Handlers{API: apiHandlers}, nil
}Special Cases
Optional Handlers:
Some handlers may be conditionally available (e.g., GitHub App only when configured):
func (h *APIHandlers) GetGitHubAppInstallURL(ctx context.Context, request GetGitHubAppInstallURLRequestObject) (GetGitHubAppInstallURLResponseObject, error) {
if h.githubApp == nil {
return GetGitHubAppInstallURL500ApplicationProblemPlusJSONResponse{
InternalErrorApplicationProblemPlusJSONResponse: NewInternalError("GitHub App not configured"),
}, nil
}
return h.githubApp.GetGitHubAppInstallURL(ctx, request)
}Raw HTTP Handlers:
Endpoints requiring raw HTTP access (webhooks) use a separate interface:
type WebhookHandlers interface {
HandleGitHubAppWebhookRaw(w http.ResponseWriter, r *http.Request)
}
// Accessed via special accessor method
func (h *APIHandlers) WebhookHandler() WebhookHandlers {
return h.webhook
}Consequences
Positive
Domain Separation:
- Each domain handler lives within its module's
handler/package - Domain-specific logic isolated from other domains
- Changes to one domain don't affect others
Testability:
- Domain handlers tested independently with mocked dependencies
- No need to instantiate entire API layer for unit tests
- Integration tests can use real composite or partial mocks
Extensibility:
- Adding new domain: Define interface, implement handler, add to composite
- Adding new endpoint: Implement in appropriate domain handler
- Compile-time error if composite doesn't delegate new method
Clean Architecture Alignment:
- Handler layer clearly separated per ADR-08
- Feature-Based modules maintained per ADR-11
- StrictServerInterface contract preserved per ADR-10
Negative
Boilerplate:
- Each StrictServerInterface method requires delegation method
- ~20+ one-liner methods in
handlers.go - Mitigation: Methods are trivial; IDE generates easily; rarely changes
Interface Synchronization:
- Domain interfaces must match StrictServerInterface signature subsets
- Adding API endpoints requires updating domain interface
- Mitigation: Compile-time error catches mismatches immediately
Mental Model:
- Developers must understand composition layer exists
- Debugging requires tracing through delegation
- Mitigation: Pattern is simple; documented in CLAUDE.md
