Compile-Time Dependency Injection: Three Patterns, Zero Runtime

· 12 min read

The Dependency Problem

Every non-trivial program needs to swap implementations:

  • Tests need mocks instead of real HTTP clients, databases, or external services
  • Environments need different configs (development vs production)
  • Platforms need OS-specific implementations (POSIX vs Windows)

Traditional solutions impose costs:

Java/C#: DI containers, reflection, runtime overhead Rust: Trait bounds everywhere, complex feature flags Go: Build tags in comments, runtime interfaces

What if you could solve all three at compile time, with zero runtime overhead, using one mechanism?


The Three Patterns

Koru addresses three distinct scopes of dependency substitution:

Pattern 1: AST Substitution (Per-Test Scope)

~test(name: "fraud check blocks payment") {
    ~fraud.check = suspicious { reason: "High risk" }
    ~payment.charge = declined { reason: "Fraud" }

    ~payment.process(amount: 10000)
    | blocked b |> assert.contains(b.reason, "Fraud")
}

Scope: Per-test isolation Use case: Unit tests with multiple mocks

Pattern 2: Conditional Impl (Environment Scope)

~[production]impl http.config {
  "url": "https://api.example.com",
  "timeout": 30
}

~[development]impl http.config {
  "url": "http://localhost:8080",
  "timeout": 5
}

Scope: Program-wide, flag-selected Use case: Environment configuration

Pattern 3: Single Impl (Platform Scope)

~abstract event filesystem.read { path: []const u8 }
| success { content: []const u8 }
| not_found {}

~impl filesystem.read = posix.read_file(path)

Scope: Program-wide, single implementation Use case: Platform abstraction, plugin systems

All three patterns compile down to zero runtime overhead. All three use the same underlying mechanism. All three are explicit and greppable.


Pattern 1: AST Substitution for Testing

Status: Design complete, implementation in progress

The key insight: Tests don’t need to run at runtime. They can run during compilation by transforming the AST.

How It Works

When you write a test in Koru:

~test(name: "payment succeeds") {
    ~payment_gateway.charge = approved { auth: "AUTH123" }
    ~inventory.reserve = reserved { id: "RES456" }

    ~payment.process(amount: 99.99)
    | success s |> assert.equals(s.auth, "AUTH123")
    | failure |> assert.fail("Should succeed")
}

The compiler:

  1. Captures the test as FlowAST - Code becomes data
  2. Extracts mock definitions - ~event = branch patterns
  3. Transforms the program AST - Substitutes mocks functionally
  4. Emits Zig code - Using the existing emitter
  5. Executes at compile-time - Zig’s comptime runs the test
  6. Fails compilation on test failure - No broken code ships

Multiple Mocks, Perfect Isolation

Each test gets its own AST copy. Mocks don’t leak between tests. No setup, no teardown, no shared state:

~test(name: "comprehensive service test") {
    ~auth.verify = authenticated { user_id: 42 }
    ~database.get = found { name: "Alice" }
    ~cache.set = stored {}
    ~notification.send = sent {}

    ~user_service.get_profile(token: "abc")
    | profile p |> assert.equals(p.name, "Alice")
}

Four mocks. One test. Complete isolation.

Why This Works

  • Tests don’t exist in the binary - Zero runtime overhead
  • Perfect isolation - Each test transforms its own AST
  • Compile-time verification - Test failures prevent compilation
  • Natural syntax - Mocks look like subflow definitions

Current status: Functional AST API exists (src/ast_functional.zig), test infrastructure in active development. See docs/TESTING-STORY.md for the complete design.


Pattern 2: Conditional Impl for Configuration

Status: Parser support complete, conditional selection next

This is where it gets interesting. Configuration that’s validated at compile time:

~abstract event http.config { source: Source }

~[production]impl http.config {
  "url": "https://api.example.com",
  "timeout": 30,
  "retry": true
}

~[development]impl http.config {
  "url": "http://localhost:8080",
  "timeout": 5,
  "retry": false
}

~[test]impl http.config {
  "url": "http://mock",
  "timeout": 1,
  "retry": false
}

Two Pieces of Magic

1. Flags Aren’t Special

production, development, and test aren’t Koru keywords. They’re just compiler flags you pass:

koruc --production myapp.kz    # Selects production impl
koruc --development myapp.kz   # Selects development impl
koruc --test myapp.kz          # Selects test impl

The compiler picks the matching impl based on flags. One binary per environment. Zero runtime selection.

2. JSON is Validated at Compile Time

That JSON? It’s not a string. It’s a Source parameter, which means:

  • The compiler parses it as code
  • Syntax errors = compile-time errors
  • Malformed JSON = compilation fails
  • Type mismatches = caught before runtime

Not limited to JSON. Could be TOML, YAML, or any DSL:

~abstract event nginx.config { source: Source }

~[production]impl nginx.config {
  server {
    listen 443 ssl;
    server_name api.example.com;
    location / {
      proxy_pass http://backend:8080;
    }
  }
}

Typo in the config? Compilation fails. Invalid syntax? Compilation fails. Wrong structure? Compilation fails.

Before runtime. Before deployment. Before production.

The Module Pattern: Hide Implementations Behind Imports

Here’s where it gets really elegant. Instead of cluttering one file with multiple flagged implementations, you can organize by environment:

// $config/main.kz - Define the interface
~abstract event http.config { source: Source }

// Import the right implementation
~[production]import "$config/production"
~[development]import "$config/development"
~[test]import "$config/test"

Each implementation lives in its own clean file:

// $config/production.kz
~impl http.config {
  "url": "https://api.example.com",
  "timeout": 30,
  "retries": 3,
  "pool_size": 20
}

// $config/development.kz
~impl http.config {
  "url": "http://localhost:8080",
  "timeout": 5,
  "retries": 1,
  "pool_size": 5
}

// $config/test.kz
~impl http.config {
  "url": "http://mock",
  "timeout": 1,
  "retries": 0,
  "pool_size": 1
}

The magic: Only ONE implementation file is imported based on the flag. The others don’t exist in the AST. They’re not compiled. They’re not in the binary. Completely deadstripped.

Build commands:

koruc --production $config/main.kz   # Only production.kz imported
koruc --development $config/main.kz  # Only development.kz imported
koruc --test $config/main.kz         # Only test.kz imported

Why this is cleaner:

  1. No flag noise - Implementation files are pure, no conditional syntax
  2. Separation of concerns - Each environment is self-contained
  3. Only one imported - Conditional import ensures single impl in AST
  4. Discoverable - All implementations visible in file tree
  5. Testable - Each impl file can be compiled independently
  6. Scalable - Complex implementations can grow without clutter
  7. Root-organized - $config/production is unambiguous, refactor-friendly

This is like Rust’s conditional compilation or Go’s build tags, but at the module level instead of the line level. The entire implementation file is selected or deadstripped as a unit.

You can even combine this with complex implementations:

// $payment/production.kz
~impl payment.charge =
    stripe.validate_key(key: api_key)
    | valid |> stripe.create_charge(amount: amount)
        | succeeded s |> log.info(message: "Charge succeeded")
            | logged |> .{ .approved = s.auth }
        | failed f |> log.error(message: f.reason)
            | logged |> .{ .declined = f }

// $payment/test.kz
~impl payment.charge =
    .{ .approved = .{ .auth = "MOCK_AUTH_123" } }

Production gets full Stripe integration with logging. Tests get instant mock. Same interface. One import controls it all.

Comparison to Other Languages

Rust:

# Cargo.toml
[features]
production = []
development = []
#[cfg(feature = "production")]
const URL: &str = "https://api.example.com";

#[cfg(feature = "development")]
const URL: &str = "http://localhost:8080";

Every constant needs #[cfg]. Every function that uses those constants might need cfg. Complexity spreads.

Go:

// +build production

package config

const URL = "https://api.example.com"

Build tags in comments (!). No IDE support. Easy to forget. Hard to grep.

Koru:

~[production]impl http.config { "url": "https://api.example.com" }
~[development]impl http.config { "url": "http://localhost:8080" }

Clean. Explicit. Greppable. Validated.

Current status: Abstract/impl syntax works today (test 064), conditional flag selection in development.


Pattern 3: Single Impl for Platform Abstraction

Status: Working NOW

This pattern is production-ready. The syntax works. The validation works. Test 064 proves it.

Logger Abstraction

~abstract event log { message: []const u8, level: []const u8 }
| done {}

// Default: stderr
~proc log {
    std.debug.print("[{s}] {s}
", .{level, message});
    return .{ .done = .{} };
}

// Production: structured JSON to file
~impl log =
    format_json(message: message, level: level, timestamp: now())
    | formatted f |> write_to_file(path: "/var/log/app.json", content: f.json)
        | written |> .{ .done = .{} }

The ~impl delegates to other events. It’s just a flow. No special syntax. No magic.

Platform-Specific Filesystem

~abstract event fs.read { path: []const u8 }
| success { content: []const u8 }
| not_found {}
| permission_denied {}

// Linux/Mac build
~impl fs.read = posix.open(path: path)
    | fd f |> posix.read_all(fd: f)
        | content c |> .{ .success = .{ .content = c } }

// Windows build
~impl fs.read = win32.CreateFile(path: path)
    | handle h |> win32.ReadFile(handle: h)
        | content c |> .{ .success = .{ .content = c } }

Different implementation per platform. Same interface. Compile-time selection.

Test Mocking

~abstract event http.get { url: []const u8 }
| success { body: []const u8 }
| error { code: u32 }

// Production: real HTTP
~impl http.get = curl.request(url: url)
    | response r |> .{ .success = .{ .body = r.body } }
    | failed f |> .{ .error = .{ .code = f.status } }

// Tests: instant mock
~impl http.get = .{ .success = .{ .body = "mock data" } }

No network calls in tests. Instant results. Same interface.

The Semantics

Abstract Event:

  • Declares the signature
  • Marks this as an extension point

Optional Default:

~proc event_name {
    // Baseline implementation
}
  • Provides fallback behavior
  • Can be overridden by ~impl

Implementation:

~impl event_name = some_flow
    | branch |> continuation
  • THE implementation (exactly one)
  • Within ~impl, event name refers to the default (delegation)
  • Outside ~impl, event name refers to the impl

Compile-Time Guarantees:

  • Abstract event without impl = compile error
  • Multiple impls = compile error
  • Delegation to non-existent default = compile error

Current status: Fully working. Test 064 validates all semantics. Validator (validate_abstract_impl.zig) enforces rules.


Why This Matters: Full Comparison

Java/C# (Runtime DI)

interface PaymentGateway {
    Result charge(Amount amount);
}

@Configuration
class AppConfig {
    @Bean
    @Profile("production")
    PaymentGateway productionGateway() {
        return new StripeGateway();
    }

    @Bean
    @Profile("test")
    PaymentGateway testGateway() {
        return new MockGateway();
    }
}

Problems:

  • Runtime reflection
  • Virtual dispatch overhead
  • DI framework dependency
  • Hard to trace: “Which impl is running?”
  • Complex lifecycle management

Rust (Trait Bounds)

trait PaymentGateway {
    fn charge(&self, amount: f64) -> Result<Auth, Error>;
}

// Everywhere this is used:
fn process_payment<P: PaymentGateway>(
    gateway: &P,
    amount: f64
) -> Result<Receipt, Error>

// And feature flags:
#[cfg(feature = "stripe")]
impl PaymentGateway for StripeGateway { ... }

#[cfg(feature = "mock")]
impl PaymentGateway for MockGateway { ... }

Problems:

  • Trait bounds propagate everywhere
  • Monomorphization bloat
  • Feature flags spread across code
  • Complex when you need dynamic selection

Go (Build Tags + Interfaces)

// payment_prod.go
// +build production

package gateway

type Gateway struct {
    // Stripe fields
}

func (g *Gateway) Charge(amount float64) Result {
    // Real implementation
}
// payment_test.go
// +build test

package gateway

type Gateway struct {
    // Mock fields
}

func (g *Gateway) Charge(amount float64) Result {
    // Mock implementation
}

Problems:

  • Build tags in comments (!!)
  • No IDE support
  • Runtime interface dispatch
  • Hard to see which impl is active

Koru

~abstract event payment.charge { amount: f64 }
| approved { auth: []const u8 }
| declined { reason: []const u8 }

~[production]impl payment.charge =
    stripe.charge(amount: amount)

~[test]impl payment.charge =
    .{ .approved = .{ .auth = "MOCK123" } }

Benefits:

  • Explicit and greppable: grep "~impl payment"
  • Zero runtime overhead: compile-time selection
  • Clean syntax: just flows, no special abstractions
  • Type safe: validated at compile time
  • Single source of truth: all impls visible

The Source-Typed DSL Magic

This deserves its own section because it’s quietly revolutionary.

Configuration as Validated Code

~abstract event database.config { source: Source }

~[production]impl database.config {
  "host": "db.example.com",
  "port": 5432,
  "pool_size": 20,
  "ssl_mode": "require"
}

What’s happening here:

  1. The source: Source parameter means parse this as code
  2. The JSON is validated at compile time
  3. Syntax errors = compilation fails
  4. The event handler can validate structure at compile time

This means:

~[production]impl database.config {
  "host": "db.example.com",
  "port": "not a number",  // COMPILE ERROR
  "pool_size": 20
}

Compilation fails. Before runtime. Before tests. Before deployment.

Not Limited to JSON

Any DSL works:

~abstract event routes { source: Source }

~impl routes {
  GET /api/users -> user.list
  POST /api/users -> user.create
  GET /api/users/:id -> user.get
  DELETE /api/users/:id -> user.delete
}

The Source type captures it. Your handler parses it. Compilation validates it.

Why This Is Powerful

Traditional config files:

  • Deployed separately from code
  • Validated at runtime (or not at all)
  • Typos discovered in production
  • Type mismatches cause runtime errors

Koru config:

  • Part of the compilation unit
  • Validated at compile time
  • Typos prevent compilation
  • Type safety from the start

This is config as code, done right.


Real-World Use Cases

These aren’t academic examples. These are patterns Koru enables today (or very soon):

Testing

  • Mock HTTP clients, databases, file systems
  • Freeze time for deterministic tests
  • Seeded random for reproducibility
  • In-memory substitutes for external services

Configuration

  • Environment-specific settings (dev/staging/prod)
  • Feature flags
  • Service discovery URLs
  • API endpoints and timeouts

Platform Abstraction

  • OS-specific filesystem operations
  • Platform-specific crypto implementations
  • Different cloud providers (AWS vs GCP vs Azure)
  • Architecture-specific optimizations

Cross-Cutting Concerns

  • Logging (stdout vs file vs cloud)
  • Metrics (StatsD vs Prometheus vs no-op)
  • Distributed tracing (OpenTelemetry vs Jaeger)
  • Rate limiting (real vs no-op for tests)
  • Circuit breakers (real vs always-open for dev)

Current Status & What’s Next

Working NOW (November 2025)

Abstract event declarations - ~abstract event syntax ✅ Single impl declarations - ~impl syntax ✅ Parser support - All syntax parses correctly ✅ AST support - is_abstract and is_impl flags ✅ Validation - validate_abstract_impl.zig enforces rules ✅ Test coverage - Test 064 proves the pattern works

You can use abstract/impl TODAY for platform abstraction and plugin systems.

In Active Development

🚧 Conditional impl with flags - ~[production]impl selection 🚧 AST substitution for testing - Test infrastructure 🚧 Source-typed config - Compile-time validation 🚧 Compiler migration - Moving compiler.coordinate to abstract/impl

Coming Soon

  • Multiple conditional impls selected by compiler flags
  • Full testing framework with AST substitution and mocking
  • Build-time config validation with custom DSLs
  • Platform abstraction examples in standard library

Why Share Now?

Because the foundation is solid. The syntax works. The semantics are proven. Test 064 validates the pattern. The vision is clear.

We’re building this systematically:

  1. Syntax ✅ Done
  2. Validation ✅ Done
  3. Single impl ✅ Working
  4. Conditional impl 🚧 Next
  5. AST substitution 🚧 In progress

Each piece builds on the last. Each piece is tested. Each piece works before we move forward.

This is how you build a compiler: methodically, transparently, with tests that prove it.


The Vision

Traditional languages force you to choose:

  • Runtime flexibility with performance cost (Java/C#)
  • Compile-time optimization with complexity cost (Rust)
  • Simplicity with safety holes (Go)

Koru gives you all three:

  • Compile-time resolution - Zero runtime overhead
  • Simple syntax - Just flows, no special abstractions
  • Type safety - Validated before runtime

Three patterns. Three scopes. One mechanism.

AST substitution for isolated test mocking. Conditional impl for environment configuration. Single impl for platform abstraction.

All explicit. All greppable. All compile-time.

No DI containers. No reflection. No virtual dispatch. No build tags in comments.

Just events, implementations, and flows.


Join Us

We’re building this now. The foundation works. The tests prove it. The roadmap is clear.

Want to see the code?

Want to follow progress?

Want to contribute?

This is an AI-first project. Every feature is designed through human-AI collaboration. The vision is human, the implementation is partnership, the tests keep us honest.

Welcome to compile-time dependency injection. Welcome to zero runtime overhead. Welcome to explicit over magic.

Welcome to Koru.