5.7 KiB
Architecture Guide
The codebase uses Clean Architecture with dependency injection to maintain separation of concerns and testability.
Clean Architecture Layers
The project is organized into four distinct layers:
1. Domain Layer - Business entities and rules (innermost) 2. Application Layer - Use cases and business logic 3. Infrastructure Layer - External services and data access 4. API Layer - HTTP handlers and routes (outermost)
Dependency Flow
Dependencies point inward only:
API → Application → Domain ← Infrastructure
- Domain layer has zero external dependencies
- Infrastructure implements domain interfaces
- Outer layers depend on inner layers, never the reverse
Layer Responsibilities
Domain Layer (src/app/{module}/domain/)
The core business logic layer.
Contains:
- Entities with business rules
- Repository interfaces (contracts)
- Domain errors
- Validation logic
Key principle: No external dependencies. Pure business logic only.
Application Layer (src/app/{module}/app/)
Orchestrates domain operations to implement use cases.
Contains:
- Service interfaces and implementations
- Request/response types
- Transaction boundaries
- Business workflow coordination
Key principle: Uses domain interfaces, never infrastructure directly.
Infrastructure Layer (src/app/{module}/infra/)
Implements domain interfaces using concrete technologies.
Contains:
- Repository implementations
- Database adapters
- External service clients
- Type conversions (domain ↔ database)
Key principle: Depends on domain interfaces. Hidden behind abstractions.
API Layer (src/api/{module}/)
Handles HTTP concerns.
Contains:
- HTTP handlers
- Route definitions
- Request validation
- Response formatting
Key principle: Thin layer that delegates to application services.
Dependency Injection
Uses uber-go/dig for automatic dependency injection.
Core Pattern
// 1. Define interface in domain
type ResourceRepository interface {
GetByID(ctx context.Context, id int32) (*Resource, error)
}
// 2. Implement in infrastructure
type resourceRepository struct {
store adapters.ResourceStore
}
// 3. Register in DI container
container.Provide(func(store adapters.ResourceStore) domain.ResourceRepository {
return NewResourceRepository(store)
})
// 4. Inject into services
container.Provide(func(repo domain.ResourceRepository) services.ResourceService {
return services.NewResourceService(repo)
})
Benefits
- Automatic dependency resolution
- Easy testing with mocks
- Clear dependency graph
- No manual wiring
Module Pattern
Each business module follows a standard structure:
src/app/{module}/
├── domain/ # Entities, interfaces
├── app/services/ # Business logic
├── infra/ # Implementations
├── cmd/init.go # Initialization
└── module.go # DI registration
Module Registration
Every module has a module.go file that registers its dependencies:
- Repositories (infrastructure → domain interface)
- Services (application layer)
- Event listeners (if applicable)
Initialization Order
Defined in src/main/cmd/init_mods.go:
- Infrastructure - Database, logging, server
- Shared Services - File storage, event bus, payments
- Authentication - Redis, Stytch, auth middleware
- Domain Modules - Organizations, billing, etc.
- API Layer - Route registration
Why order matters: Each phase depends on previous phases being initialized.
Resolver Pattern
Bridges authentication with domain modules without creating circular dependencies.
Problem
Auth middleware needs to convert provider IDs (Stytch) to database IDs, but can't depend on domain modules directly.
Solution
Define minimal interfaces in auth package:
// Auth defines what it needs
type OrganizationResolver interface {
ResolveByProviderID(ctx context.Context, providerID string) (int32, error)
}
Domain modules implement via adapters:
// Module provides implementation
type orgResolverAdapter struct {
repo domain.ResourceRepository
}
Wired together in init_mods.go during initialization.
Best Practices
Constructor Pattern
Always return interfaces, not concrete types:
// ✅ Good
func NewResourceService(repo domain.ResourceRepository) services.ResourceService {
return &resourceService{repo: repo}
}
// ❌ Bad
func NewResourceService(repo domain.ResourceRepository) *resourceService {
return &resourceService{repo: repo}
}
Context Handling
Context is always the first parameter:
func (s *service) CreateResource(ctx context.Context, req *Request) error
Error Wrapping
Add context to errors before returning:
if err := s.repo.Create(ctx, resource); err != nil {
return fmt.Errorf("failed to create resource: %w", err)
}
Explicit Dependencies
All dependencies through constructor parameters:
func NewResourceService(
repo domain.ResourceRepository,
eventBus eventbus.EventBus,
logger logger.Logger,
) services.ResourceService
Never use global variables or hidden dependencies.
File Locations
| Pattern | File |
|---|---|
| DI container setup | src/main/cmd/root.go |
| Module initialization order | src/main/cmd/init_mods.go |
| Module DI registration | src/app/*/module.go |
| Package initialization | src/pkg/*/cmd/init.go |
| API route setup | src/api/provider.go |
Next Steps
- Database operations: See Database Guide
- Authentication setup: See Authentication Guide
- Building APIs: See API Development Guide