Single Responsibility Rewarded: Why Granular Events Make Your Code Faster
“A function should do one thing, do it well, and do it only.” — Every programming book ever
The Principle Everyone Knows, Nobody Follows
The Single Responsibility Principle (SRP) is programming gospel. Every developer can recite it:
“A class should have only one reason to change.” “A function should do one thing.” “Separate concerns.”
We all nod sagely. We all agree.
And then we write this:
public class OrderService
{
public async Task<bool> ProcessOrder(Guid orderId)
{
// Validate
var order = await _repository.GetOrder(orderId);
if (order == null) return false;
if (order.Items.Count == 0) return false;
// Check inventory
foreach (var item in order.Items)
{
var stock = await _inventory.CheckStock(item.ProductId);
if (stock < item.Quantity) return false;
}
// Process payment
var paymentResult = await _payment.ProcessPayment(order.Total);
if (!paymentResult.Success) return false;
// Reserve inventory
foreach (var item in order.Items)
{
await _inventory.ReserveStock(item.ProductId, item.Quantity);
}
// Send confirmation
await _email.SendConfirmation(order.CustomerEmail);
// Update analytics
await _analytics.TrackPurchase(order);
// Update order status
order.Status = OrderStatus.Completed;
await _repository.UpdateOrder(order);
return true;
}
} This method does:
- Validation
- Inventory checking
- Payment processing
- Inventory reservation
- Email sending
- Analytics tracking
- Database updates
Seven responsibilities. Seven reasons to change. Seven violations of SRP.
Why do we do this? Because the language fights us.
Why Languages Punish SRP
Let’s say you want to extract these into separate methods. In C#, you’d need:
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IInventoryService _inventory;
private readonly IPaymentService _payment;
private readonly IEmailService _email;
private readonly IAnalyticsService _analytics;
// Constructor hell
public OrderService(
IOrderRepository repository,
IInventoryService inventory,
IPaymentService payment,
IEmailService email,
IAnalyticsService analytics)
{
_repository = repository;
_inventory = inventory;
_payment = payment;
_email = email;
_analytics = analytics;
}
public async Task<OrderResult> ProcessOrder(Guid orderId)
{
var validationResult = await ValidateOrder(orderId);
if (!validationResult.IsValid) return OrderResult.Invalid();
var inventoryResult = await CheckInventory(validationResult.Order);
if (!inventoryResult.Available) return OrderResult.OutOfStock();
var paymentResult = await ProcessPayment(validationResult.Order);
if (!paymentResult.Success) return OrderResult.PaymentFailed();
// ... more methods
}
private async Task<ValidationResult> ValidateOrder(Guid orderId)
{
var order = await _repository.GetOrder(orderId);
// ...
}
private async Task<InventoryResult> CheckInventory(Order order)
{
// ...
}
// More private methods...
} Look what happened:
- More boilerplate - interfaces, DI, constructors
- Still one giant class - methods are trapped together
- Unclear dependencies - which method needs which service?
- Hard to test independently - can’t test
CheckInventorywithout mocking the whole class - No optimization opportunities - compiler sees one big blob
Following SRP made the code WORSE. So we don’t do it.
The language punished us for trying.
How Koru Rewards Granularity
In Koru, small events are the default:
~event validateOrder { order_id: OrderId }
| valid { order: Order }
| not_found {}
| empty_cart {}
~event checkInventory { order: Order }
| available {}
| out_of_stock { product_id: ProductId }
~event processPayment { order: Order }
| paid { transaction_id: TransactionId }
| declined { reason: []const u8 }
~event reserveInventory { order: Order }
| reserved {}
| reservation_failed { reason: []const u8 }
~event sendConfirmation { order: Order }
| sent {}
| email_failed { reason: []const u8 }
~event trackPurchase { order: Order }
| tracked {}
~event updateOrderStatus { order: Order, status: OrderStatus }
| updated {}
| update_failed { reason: []const u8 } Now compose them:
~validateOrder(order_id: id)
| valid order |> checkInventory(order: order.order)
| available |> processPayment(order: order.order)
| paid tx |> reserveInventory(order: order.order)
| reserved |> sendConfirmation(order: order.order)
| sent |> trackPurchase(order: order.order)
| tracked |> updateOrderStatus(order: order.order, status: .completed)
| updated |> _
| update_failed e |> logError(msg: e.reason)
| update_failed e |> logError(msg: e.reason)
| email_failed e |> logWarning(msg: e.reason)
// Email failure doesn't stop the order
|> trackPurchase(order: order.order)
| tracked |> updateOrderStatus(order: order.order, status: .completed)
| reservation_failed e |> refundPayment(tx: tx.transaction_id)
| refunded |> logError(msg: e.reason)
| declined r |> logError(msg: r.reason)
| out_of_stock p |> logError(msg: "Out of stock")
| not_found |> logError(msg: "Order not found")
| empty_cart |> logError(msg: "Empty cart") Look at what we got:
- Each event has ONE responsibility - validate, check inventory, process payment, etc.
- Zero boilerplate - no interfaces, no DI, no constructor hell
- Clear dependencies - each event declares exactly what it needs
- Independently testable - test
checkInventoryin isolation - Self-documenting - read the flow, understand the logic
- All error cases explicit - every branch handled
And here’s the kicker: the compiler can optimize the hell out of this.
Why the Compiler Loves Granular Events
When you write small, focused events, the Koru compiler can:
1. Inline Aggressively
Small events compile to small Zig functions. The Zig compiler can inline them completely:
~validateOrder(order_id: id)
| valid order |> checkInventory(order: order.order) Compiles to (simplified):
// After inlining
const validate_result = validateOrder(.{ .order_id = id });
switch (validate_result) {
.valid => |order| {
const inventory_result = checkInventory(.{ .order = order.order });
// Both functions inlined, no call overhead
},
// ...
} 2. Reorder Pure Events
If an event is pure (no side effects), the compiler can reorder it:
~calculateTax(order: order)
| tax_amount t |> calculateShipping(order: order)
| shipping_cost s |> computeTotal(tax: t.tax_amount, shipping: s.shipping_cost) The compiler can execute calculateTax and calculateShipping in parallel or in any order because they’re pure and independent.
3. Eliminate Dead Branches
If the compiler proves a branch can’t happen, it eliminates it:
~event isPositive { value: i32 }
| yes {}
| no {}
~proc isPositive {
if (value > 0) return .{ .yes = .{} };
return .{ .no = .{} };
}
// Later in code with constant:
~isPositive(value: 42)
| yes |> doSomething()
| no |> doSomethingElse() // Eliminated at comptime! The compiler knows 42 > 0, so the no branch is eliminated entirely.
4. Optimize Each Event Independently
Small events = small optimization problems. The compiler can:
- Specialize events for specific types
- Constant-fold parameters
- Eliminate redundant checks
- Vectorize operations
Large monolithic functions? The compiler gives up. Too complex.
Small focused events? The compiler optimizes aggressively.
The more granular your events, the faster your code runs.
The SRP Reward Loop
In traditional languages:
- SRP → More code → More boilerplate → Slower development → Skip SRP
In Koru:
- SRP → Granular events → Better optimization → Faster code → Natural SRP
Following SRP makes your code:
- Easier to write (compose small events)
- Easier to test (test each event independently)
- Easier to understand (each event does one thing)
- Faster to run (compiler optimizes granular code better)
The language rewards you for doing the right thing.
Bounded Contexts: Perfect Isolation
Here’s another benefit of granular events: they’re perfect bounded contexts.
Each event is completely self-contained:
- Declares its inputs
- Declares all possible outputs
- Has a single implementation (proc)
- No hidden dependencies
This makes them trivial to implement with AI:
Human: "Implement checkInventory event.
Input: Order with items.
Outputs: available if all in stock, out_of_stock with product_id if not.
Query the inventory service for each item."
AI: [writes proc implementation] The event signature is the complete specification. The AI doesn’t need to understand the whole codebase—just this one bounded context.
Want to know more about how events create perfect bounded contexts for AI-assisted development? We’re writing a full article on that—stay tuned!
The Comparison
Let’s see the same order processing in C# vs Koru:
C# (Monolithic, Optimizable as Single Blob)
public async Task<bool> ProcessOrder(Guid orderId)
{
var order = await _repository.GetOrder(orderId);
if (order == null || order.Items.Count == 0) return false;
foreach (var item in order.Items) {
var stock = await _inventory.CheckStock(item.ProductId);
if (stock < item.Quantity) return false;
}
var paymentResult = await _payment.ProcessPayment(order.Total);
if (!paymentResult.Success) return false;
foreach (var item in order.Items) {
await _inventory.ReserveStock(item.ProductId, item.Quantity);
}
await _email.SendConfirmation(order.CustomerEmail);
await _analytics.TrackPurchase(order);
order.Status = OrderStatus.Completed;
await _repository.UpdateOrder(order);
return true;
} - 7 responsibilities
- Hard to test
- Can’t optimize individual steps
- Error handling is
return false(what failed?)
Koru (Granular, Optimizable at Each Step)
~validateOrder(order_id: id)
| valid order |> checkInventory(order: order.order)
| available |> processPayment(order: order.order)
| paid tx |> reserveInventory(order: order.order)
| reserved |> sendConfirmation(order: order.order)
| sent |> trackPurchase(order: order.order)
| tracked |> updateOrderStatus(order: order.order, status: .completed)
| updated |> _
| update_failed e |> logError(msg: e.reason)
| email_failed e |> continueAnyway()
| reservation_failed e |> refundAndLog(tx: tx.transaction_id, msg: e.reason)
| declined r |> logError(msg: r.reason)
| out_of_stock p |> notifyCustomer(product: p.product_id)
| not_found |> logError(msg: "Order not found")
| empty_cart |> logError(msg: "Empty cart") - 1 responsibility per event
- Each event testable independently
- Compiler optimizes each event + composition
- All error cases explicit and handled
The Koru version is more granular AND faster.
The Controversial Claim
Here’s what we’re saying:
In most languages, following SRP makes your code slower (more indirection, harder for compiler to optimize).
In Koru, following SRP makes your code faster (more optimization opportunities, better inlining).
This inverts the usual trade-off. Clean code vs fast code? In Koru, clean code IS fast code.
The Path Forward
If you’re used to monolithic methods because “it’s faster” or “less boilerplate,” Koru offers a different path:
- Write small, focused events - each does one thing
- Compose them naturally - continuation syntax makes it trivial
- Let the compiler optimize - granular code = better optimization
- Get both clarity and performance - no trade-off required
The single responsibility principle isn’t just good architecture—in Koru, it’s a performance optimization.
What’s Next?
Want to dive deeper into the concepts behind this?
- Events Are Monads: The Free Monad at the Heart of Koru - Why small composable events work
- The Tyranny of General Purpose - Why other languages fight SRP
- Bounded Contexts and AI (coming soon) - How events create perfect isolation for AI-assisted development
Think granular events are too much overhead? Want to defend monolithic methods? Let’s discuss! Join us on GitHub Discussions or Discord.
Run the benchmarks yourself. Granular Koru events match or beat monolithic C# methods. The compiler is that good.