Compile-Time Dependency Injection: Three Patterns, Zero Runtime
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:
- Captures the test as FlowAST - Code becomes data
- Extracts mock definitions -
~event = branchpatterns - Transforms the program AST - Substitutes mocks functionally
- Emits Zig code - Using the existing emitter
- Executes at compile-time - Zig’s comptime runs the test
- 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:
- No flag noise - Implementation files are pure, no conditional syntax
- Separation of concerns - Each environment is self-contained
- Only one imported - Conditional import ensures single impl in AST
- Discoverable - All implementations visible in file tree
- Testable - Each impl file can be compiled independently
- Scalable - Complex implementations can grow without clutter
- Root-organized -
$config/productionis 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:
- The
source: Sourceparameter means parse this as code - The JSON is validated at compile time
- Syntax errors = compilation fails
- 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:
- Syntax ✅ Done
- Validation ✅ Done
- Single impl ✅ Working
- Conditional impl 🚧 Next
- 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?
- Test 064: Abstract/Impl - Working today
- TESTING-STORY.md - Complete design for AST substitution
- validate_abstract_impl.zig - Enforcement implementation
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.