Skip to main content
Kosh implements a service-oriented architecture with dependency injection, separating business logic from orchestration and data access.

Service Interfaces

All services are defined as interfaces in builder/services/interfaces.go, following Go’s implicit interface satisfaction.

PostService

Handles all post-related operations: parsing, rendering, and indexing.
type PostService interface {
    Process(ctx context.Context, shouldForce, forceSocialRebuild, outputMissing bool) (*PostResult, error)
    ProcessSingle(ctx context.Context, path string) error
    RenderCachedPosts()
}
Key Responsibilities:
  • Parse markdown files with Goldmark
  • Render HTML with SSR (D2 diagrams, KaTeX math)
  • Extract metadata and table of contents
  • Build search index with BM25 scoring
  • Generate social cards
  • Manage post dependencies (templates, includes)
Implementation Details:
type postServiceImpl struct {
    cfg            *config.Config
    cache          CacheService
    renderer       RenderService
    logger         *slog.Logger
    metrics        *metrics.BuildMetrics
    md             goldmark.Markdown
    nativeRenderer *native.Renderer
    sourceFs       afero.Fs
    destFs         afero.Fs
    diagramAdapter *cache.DiagramCacheAdapter
}
The PostService uses worker pools to parallelize markdown parsing across CPU cores, significantly improving build times for large sites.

CacheService

Provides a thread-safe wrapper around the cache manager.
type CacheService interface {
    // Read operations
    GetPost(id string) (*cache.PostMeta, error)
    ListAllPosts() ([]string, error)
    GetPostByPath(path string) (*cache.PostMeta, error)
    GetPostsByIDs(ids []string) (map[string]*cache.PostMeta, error)
    GetSearchRecords(ids []string) (map[string]*cache.SearchRecord, error)
    GetHTMLContent(post *cache.PostMeta) ([]byte, error)
    
    // Write operations
    StoreHTML(content []byte) (string, error)
    StoreHTMLForPost(post *cache.PostMeta, content []byte) error
    BatchCommit(posts []*cache.PostMeta, records map[string]*cache.SearchRecord, deps map[string]*cache.Dependencies) error
    DeletePost(postID string) error
    
    // Dirty tracking
    MarkDirty(postID string)
    IsDirty(postID string) bool
    
    // Lifecycle
    Stats() (*cache.CacheStats, error)
    Close() error
}
Key Features:
  • Dirty tracking with sync.Map for lock-free access
  • Batch operations to minimize BoltDB transactions
  • Type-safe reads using generics
  • In-memory LRU cache with 5-minute TTL for hot data
Implementation:
type cacheServiceImpl struct {
    manager *cache.Manager
    logger  *slog.Logger
    
    // Dirty tracking using sync.Map for thread safety
    dirty sync.Map
}
The CacheService uses sync.Map instead of map[string]bool with mutex to provide lock-free dirty tracking in high-concurrency scenarios.

AssetService

Manages static assets: CSS, JavaScript, images, fonts, and other files.
type AssetService interface {
    Build(ctx context.Context) error
}
Responsibilities:
  • Copy static files from theme and site directories
  • Bundle and minify CSS/JS with esbuild
  • Compress images (PNG/JPG → WebP)
  • Generate content hashes for cache-busting
  • Populate Assets map for template rendering
Build Process:
func (s *assetServiceImpl) Build(ctx context.Context) error {
    var wg sync.WaitGroup
    wg.Add(2)
    
    // 1. Static Copy (excluding source CSS/JS)
    go func() {
        defer wg.Done()
        // Copy theme static assets
        // Copy site static assets
        // Handle special files (wasm_exec.js)
    }()
    
    // 2. CSS/JS Bundling
    go func() {
        defer wg.Done()
        // Bundle and minify with esbuild
        // Generate hashed filenames
        // Populate Assets map
    }()
    
    wg.Wait()
    return nil
}
Asset processing runs in parallel, but must complete before post rendering to ensure the Assets map is populated.

RenderService

Wraps the HTML template renderer with asset management.
type RenderService interface {
    RenderPage(path string, data models.PageData)
    RenderIndex(path string, data models.PageData)
    Render404(path string, data models.PageData)
    RenderGraph(path string, data models.PageData)
    
    RegisterFile(path string)
    SetAssets(assets map[string]string)
    GetAssets() map[string]string
    GetRenderedFiles() map[string]bool
    ClearRenderedFiles()
}
Key Features:
  • Template caching for fast re-renders
  • Asset map injection for cache-busted URLs
  • File registration for VFS sync tracking
  • Multiple render targets (pages, indexes, 404, graph)
Template Data:
type PageData struct {
    Title          string
    Description    string
    Content        template.HTML
    Meta           map[string]interface{}
    BaseURL        string
    Permalink      string
    Image          string
    TOC            []TOCEntry
    Config         *config.Config
    Assets         map[string]string  // Injected by RenderService
    // ... versioning, navigation, etc.
}

Service Composition

Services are composed in builder/run/builder.go through constructor injection:
func newBuilderWithConfig(cfg *config.Config) *Builder {
    // Initialize infrastructure
    logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))
    buildMetrics := metrics.NewBuildMetrics()
    sourceFs := afero.NewOsFs()
    destFs := afero.NewMemMapFs()
    
    // Open cache
    cacheManager, err := cache.OpenWithTimeout(cfg.CacheDir, cfg.IsDev, cacheTimeout)
    if err != nil {
        logger.Warn("Failed to open cache database, using in-memory cache", "error", err)
    }
    
    // Create cache adapter
    diagramAdapter := cache.NewDiagramCacheAdapter(cacheManager)
    
    // Create native renderer
    nativeRenderer := native.New()
    
    // Create markdown parser
    md := mdParser.New(cfg.BaseURL, nativeRenderer, diagramCache)
    rnd := renderer.New(cfg.CompressImages, destFs, cfg.TemplateDir, logger)
    
    // Wire services together
    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,
    )
    
    return &Builder{
        cfg:            cfg,
        cacheService:   cacheSvc,
        postService:    postSvc,
        assetService:   assetSvc,
        renderService:  renderSvc,
        diagramAdapter: diagramAdapter,
        logger:         logger,
        metrics:        buildMetrics,
        SourceFs:       sourceFs,
        DestFs:         destFs,
        md:             md,
    }
}

Service Lifecycle

Initialization

  1. Config Loading: Parse kosh.yaml and CLI flags
  2. Cache Opening: Connect to BoltDB (or create new)
  3. Service Creation: Instantiate services with dependencies
  4. Validation: Check theme existence, create directories

Build Execution

  1. Builder.Build(ctx): Main entry point
  2. Asset Processing: assetService.Build(ctx)
  3. Post Processing: postService.Process(ctx, ...)
  4. Global Pages: Pagination, tags, graph, metadata
  5. PWA Generation: Manifest, service worker, icons
  6. VFS Sync: Flush memory file system to disk

Cleanup

  1. Save Caches: Flush diagram and metadata caches
  2. Increment Build Count: Track successful builds
  3. Close Resources: Close BoltDB connections
  4. Print Metrics: Display build statistics
func Run(args []string) {
    b := NewBuilder(args)
    defer b.Close()         // Close BoltDB
    defer b.SaveCaches()    // Flush caches
    if err := b.Build(context.Background()); err != nil {
        b.logger.Error("Build failed", "error", err)
    }
}

Testing Services

The service layer’s interface-based design enables easy mocking:
type mockCacheService struct {
    posts map[string]*cache.PostMeta
}

func (m *mockCacheService) GetPost(id string) (*cache.PostMeta, error) {
    post, ok := m.posts[id]
    if !ok {
        return nil, nil
    }
    return post, nil
}

// Use in tests
func TestPostService(t *testing.T) {
    mockCache := &mockCacheService{
        posts: map[string]*cache.PostMeta{
            "test-id": {Title: "Test Post"},
        },
    }
    
    svc := services.NewPostService(
        cfg, mockCache, mockRender, logger, metrics,
        md, nativeRenderer, sourceFs, destFs, diagramAdapter,
    )
    
    // Test service methods...
}

Error Handling in Services

Services follow consistent error handling patterns:
  1. Wrap errors with context: fmt.Errorf("failed to parse post: %w", err)
  2. Log errors with structured fields: logger.Error("msg", "key", value, "error", err)
  3. Return errors to caller: Let orchestration layer decide how to handle
  4. Continue on non-critical errors: Log warnings but keep building
func (s *postServiceImpl) Process(ctx context.Context, ...) (*PostResult, error) {
    // Non-critical: log and continue
    if err := s.cache.DeletePost(id); err != nil {
        s.logger.Warn("Failed to delete stale post", "id", id, "error", err)
    }
    
    // Critical: return error
    if err := s.cache.BatchCommit(posts, records, deps); err != nil {
        return nil, fmt.Errorf("failed to commit cache batch: %w", err)
    }
    
    return result, nil
}

Performance Optimizations

Worker Pools

Services use utils.WorkerPool[T] for bounded concurrency:
parsePool := utils.NewWorkerPool(ctx, numWorkers, func(task PostTask) {
    // Parse markdown
    // Render HTML
    // Extract metadata
})
parsePool.Start()
for _, file := range files {
    parsePool.Submit(PostTask{path: file})
}
parsePool.Stop()  // Wait for completion

Object Pooling

Reduce GC pressure with sync.Pool:
buf := utils.SharedBufferPool.Get()
defer utils.SharedBufferPool.Put(buf)

if err := s.md.Renderer().Render(buf, source, docNode); err != nil {
    return err
}
htmlContent := buf.String()

Batch Operations

Group database writes to minimize transactions:
var newPosts []*cache.PostMeta
var newRecords map[string]*cache.SearchRecord

// Collect all changes
for _, post := range posts {
    newPosts = append(newPosts, post.Meta)
    newRecords[post.ID] = post.SearchRecord
}

// Single transaction
if err := s.cache.BatchCommit(newPosts, newRecords, deps); err != nil {
    return err
}

Next Steps

Build docs developers (and LLMs) love