web-production-saas-starter/go-b2b-starter/docs/architecture.md
2025-12-16 18:54:41 +04:00

238 lines
5.7 KiB
Markdown

# 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](https://github.com/uber-go/dig) for automatic dependency injection.
### Core Pattern
```go
// 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`:
1. **Infrastructure** - Database, logging, server
2. **Shared Services** - File storage, event bus, payments
3. **Authentication** - Redis, Stytch, auth middleware
4. **Domain Modules** - Organizations, billing, etc.
5. **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:
```go
// Auth defines what it needs
type OrganizationResolver interface {
ResolveByProviderID(ctx context.Context, providerID string) (int32, error)
}
```
Domain modules implement via adapters:
```go
// 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:
```go
// ✅ 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:
```go
func (s *service) CreateResource(ctx context.Context, req *Request) error
```
### Error Wrapping
Add context to errors before returning:
```go
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:
```go
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](./database.md)
- **Authentication setup**: See [Authentication Guide](./authentication.md)
- **Building APIs**: See [API Development Guide](./api-development.md)