Kosh follows a clean architecture pattern with clear separation of concerns across three distinct layers. This design enables testability, maintainability, and flexibility.
Architectural Layers
1. Orchestration Layer (builder/run/)
Coordinates the build process and manages the build lifecycle.
| File | Responsibility |
|---|
builder.go | Builder initialization and dependency injection container |
build.go | Main build orchestration with context support |
incremental.go | Watch mode and single-post fast rebuilds |
pipeline_*.go | Specialized pipelines (assets, posts, meta, PWA, pagination) |
Key Features:
- Context-aware cancellation support
- Build locking to prevent concurrent builds
- Dependency graph resolution
- Phase coordination (assets → posts → global pages → PWA)
2. Service Layer (builder/services/)
Provides business logic through well-defined interfaces.
| Service | Interface | Implementation |
|---|
| PostService | PostService | Markdown parsing, rendering, and indexing |
| CacheService | CacheService | Thread-safe cache operations with sync.Map |
| AssetService | AssetService | Static asset management and processing |
| RenderService | RenderService | HTML template rendering wrapper |
All services are defined as interfaces in services/interfaces.go, enabling easy mocking for tests and flexible implementation swapping.
3. Data Access Layer (builder/cache/)
Handles all persistent storage operations.
| Component | Purpose |
|---|
cache.go | BoltDB operations with generics |
types.go | Data structures and encoding |
store.go | Content-addressed file storage |
cache_reads.go | Read operations with LRU caching |
cache_writes.go | Write operations with batching |
Build Pipeline Flow
The build process follows a strict order to ensure data dependencies are satisfied:
Static assets MUST complete before post rendering because templates use the Assets map for hashed CSS/JS filenames. This prevents race conditions that could cause 404 errors.
Dependency Injection Pattern
Kosh uses constructor-based dependency injection with the Builder struct as the composition root.
Builder as DI Container
type Builder struct {
cfg *config.Config
// Services (injected dependencies)
cacheService services.CacheService
postService services.PostService
assetService services.AssetService
renderService services.RenderService
// Infrastructure
logger *slog.Logger
metrics *metrics.BuildMetrics
SourceFs afero.Fs
DestFs afero.Fs
}
Service Initialization
Services are wired together in builder/run/builder.go:newBuilderWithConfig():
// Create Services
var cacheSvc services.CacheService
if cacheManager != nil {
cacheSvc = services.NewCacheService(cacheManager, logger)
}
renderSvc := services.NewRenderService(rnd, logger)
assetSvc := services.NewAssetService(sourceFs, destFs, cfg, renderSvc, logger)
postSvc := services.NewPostService(
cfg, cacheSvc, renderSvc, logger, buildMetrics,
md, nativeRenderer, sourceFs, destFs, diagramAdapter,
)
Benefits of This Pattern
| Benefit | Description |
|---|
| Testability | Services can be easily mocked for unit tests |
| Separation of Concerns | Each service has a single, well-defined responsibility |
| Flexibility | Implementations can be swapped without changing business logic |
| Explicit Dependencies | Constructor signatures clearly show what each service needs |
The DI pattern was introduced in Phase 2 of development as part of the architecture refactoring initiative.
File System Abstraction
Kosh uses afero for file system operations, enabling:
- In-Memory Builds: The entire site is built in a memory-mapped file system (
MemMapFs)
- Atomic Writes: Files are synced to disk only after the build completes successfully
- Fast Rebuilds: Changed files can be identified without disk I/O
- Testability: Tests can run against in-memory file systems
// Initialize Filesystems
sourceFs := afero.NewOsFs() // Real OS for reading source
destFs := afero.NewMemMapFs() // Memory for building output
Context Propagation
All long-running operations accept context.Context for graceful shutdown:
func (b *Builder) Build(ctx context.Context) error {
// Check for cancellation early
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// ... build logic ...
}
Signal Handling:
SIGINT (Ctrl+C) triggers graceful shutdown
SIGTERM also triggers graceful shutdown
- 5-second timeout for cleanup operations
Error Handling Strategy
Kosh follows Go best practices for error handling:
- Wrap errors with context:
fmt.Errorf("context: %w", err)
- Never ignore errors: Always handle or log appropriately
- Structured logging: Use
slog for error reporting
- Fail fast on critical errors: Exit immediately on startup failures
- Continue on recoverable errors: Log warnings but continue building
if err := b.assetService.Build(ctx); err != nil {
b.logger.Error("Failed to build assets", "error", err)
// Continue anyway - posts might still work
}
Parallel Processing
Kosh uses worker pools for CPU-intensive operations:
- Post parsing: Parallel markdown parsing
- Asset processing: Concurrent image compression
- Social card generation: Background rendering
- Search indexing: Parallel tokenization
Memory Management
Object pooling reduces GC pressure:
BufferPool: Reusable bytes.Buffer for markdown rendering
EncodedPostPool: Reusable slices for batch commits
sync.Pool: Used throughout for temporary allocations
Build Metrics
The metrics package tracks:
- Total build duration
- Cache hit/miss ratio
- Posts processed count
- Individual phase timings
Use --cpuprofile and --memprofile flags to identify performance bottlenecks in your builds.
Thread Safety
Kosh is designed for concurrent builds:
- sync.Map for lock-free dirty tracking
- sync.RWMutex for BoltDB access
- Worker pools for bounded concurrency
- Atomic operations for counters and flags
Next Steps