Deterministic event-driven processes for .NET

Make distributed
business logic boring.

Pure functional DDD, declarative sagas, and versioned contracts—designed so the correct path is the one your code naturally takes.

dotnet add package Wyrd
v0.5.0-rc.1 · built for .NET 10 and C# 14
Process.StartOn<OrderPlaced>()
.Then(Stock.Reserve).Retry(3.Times)
.Then(Payments.Charge).Retry(3.Times)
.Await<PaymentConfirmed>()

01 / THE PROBLEM

Sagas fail in the spaces
between the happy paths.

A duplicated message charges twice. A missing timeout waits forever. A failed compensation leaves stock reserved. It all compiles—and often passes the tests.

duplicate deliverypartial failurecontract driftlate eventslost timeouts

02 / THE MODEL

Business rules first.
Readable by the business.

Wyrd draws a hard line between decisions and effects, then makes that line useful.

01

Pure decisions

Business rules stay as ordinary functions. No database, publisher, clock, or hidden I/O.

02

Effects owned by Wyrd

Retries, timeouts, idempotency, compensation, and delivery belong to the framework.

03

Processes are data

Run, draw, inspect, simulate and replay one model, in memory or durable.

04

Contracts that resist drift

Roslyn analyzers catch broken public events while the change is still in your editor.

THE CHANGE IN PERSPECTIVE

Keep the domain.
Change the mechanics.

DDD starts with understanding the business. Wyrd gives that understanding a more explicit shape in code.

An order can be cancelled. A payment can be declined. A reservation can expire. These are the decisions a product team needs to discuss, but their implementation often disappears inside handlers, transactions, and message plumbing.

Wyrd separates the rule from its execution: a command and current state produce a fact or a rejection. The state evolves from accepted facts. A process connects those local decisions into a business policy.

Your language, aggregate boundaries, and invariants still matter. The shift is that the decision no longer needs to know how a message is delivered or a state change is stored. You can review the rule on its own, test it from facts, and inspect the flow that connects it to the rest of the business.

03 / THE PROCESS

Six lines that describe the business.

The process definition is immutable data. Steps are pure mappings from facts to intentions; Wyrd owns the messy mechanics around them.

  • Retries preserve the same message identity
  • Compensation runs in reverse order
  • Time advances on a virtual clock in tests
Explore process modelling
OrderFulfillment.cs
C#
public static Process OrderFulfillment => Process
    .StartOn<OrderPlaced>(e => e.OrderId)
    .Then(Stock.Reserve).Retry(5.Times).Undo(Stock.Release)
    .Then(Payments.Charge).Retry(5.Times, Backoff.Exponential).Undo(Payments.Refund)
    .Await<PaymentConfirmed>(e => e.OrderId).Within(15.Minutes)
    .Then(Shipping.Ship).Retry(5.Times)
    .OnCompensated(Orders.Cancel).Retry(5.Times);

04 / THE THREE NORNS

Future. Past. Present.

In Norse myth, wyrd—fate—is woven by three Norns. The framework follows the same shape.

Available

what will be

Skuld

Simulate

Run your scenario in hundreds of hostile futures, with faults, duplicates, and reordering, and replay any broken one by its seed.

Available

what was

Urd

Replay

Capture a process instance, in memory or from the database, and replay its recorded replies and clock as a deterministic local test.

Available

what is

Verdandi

Run

Run the same module durable on EF Core (PostgreSQL, SQL Server): one transaction per message, integrations as the only I/O, explicit retention, operators, and RabbitMQ between services.

05 / PUBLIC CONTRACTS

Break the contract.
Break the build.

Integration events are public APIs. Wyrd snapshots their shape and explains what changed, why it is unsafe, and how to fix it.

WYRD0201

Breaking change: field Amount changed from decimal to double.

Read about versioned contracts
Payments.cs
C#
// An intention sent to exactly one owner
public sealed record ChargePayment(string OrderId, decimal Amount) : ICommand;

// An internal fact inside one bounded context
public sealed record PaymentCharged(string OrderId, decimal Amount) : IDomainEvent;

// A versioned public contract across boundaries
[IntegrationEvent("payments.payment-charged", Version = 1)]
public sealed record PaymentChargedV1(string OrderId, decimal Amount, string Currency)
    : IIntegrationEvent;

START WEAVING

Event-driven that's
no longer weird.

Wyrd is pre-1.0: try the model in memory, run it durable, read the source, and help shape 1.0.