Skip to main content
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.
FileResponsibility
builder.goBuilder initialization and dependency injection container
build.goMain build orchestration with context support
incremental.goWatch mode and single-post fast rebuilds
pipeline_*.goSpecialized 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.
ServiceInterfaceImplementation
PostServicePostServiceMarkdown parsing, rendering, and indexing
CacheServiceCacheServiceThread-safe cache operations with sync.Map
AssetServiceAssetServiceStatic asset management and processing
RenderServiceRenderServiceHTML 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.
ComponentPurpose
cache.goBoltDB operations with generics
types.goData structures and encoding
store.goContent-addressed file storage
cache_reads.goRead operations with LRU caching
cache_writes.goWrite 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

BenefitDescription
TestabilityServices can be easily mocked for unit tests
Separation of ConcernsEach service has a single, well-defined responsibility
FlexibilityImplementations can be swapped without changing business logic
Explicit DependenciesConstructor 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:
  1. In-Memory Builds: The entire site is built in a memory-mapped file system (MemMapFs)
  2. Atomic Writes: Files are synced to disk only after the build completes successfully
  3. Fast Rebuilds: Changed files can be identified without disk I/O
  4. 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:
  1. Wrap errors with context: fmt.Errorf("context: %w", err)
  2. Never ignore errors: Always handle or log appropriately
  3. Structured logging: Use slog for error reporting
  4. Fail fast on critical errors: Exit immediately on startup failures
  5. 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
}

Performance Considerations

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

Build docs developers (and LLMs) love