October 14, 2026
An Order Saga on Kafka and Cosmos
One ASP.NET Core process runs an order state machine: Kafka carries commands and facts, and Cosmos DB stores the saga document.
TL;DR — One ASP.NET Core process, on MassTransit 8.5.11 and .NET 8, runs an order state machine and a static page. Kafka carries the commands and the facts. Cosmos DB stores the saga document. The page stays on this machine.
Stack — C#, .NET 8, MassTransit 8.5.11, Kafka, Cosmos DB, NUnit
Links — Source
A caller opens the page on port 5080, creates an order, and sees the state, the commands that state allows, the events sent to the saga, and the facts that come back. The order moves through Submitted, Paid, Shipped, Completed, and Cancelled. The projects target net8.0.
MassTransit expects a broker such as RabbitMQ, Azure Service Bus, or Amazon SQS to own the saga, with Kafka only at the edge, and its transactional outbox in SQL or MongoDB. This sample keeps the saga on the Kafka rider. The in-memory bus exists only because the rider requires a bus. It has no receive endpoints. The state machine does not produce to Kafka.
The saga document and PendingFacts commit together. The first event inserts the document with CreateItem. Later transitions replace it with ReplaceItem and If-Match. A stale ETag is HTTP 412. After that commit, PendingFactDispatcher produces each fact and then removes it. If the process dies between those two steps, the next pass produces the fact again. The page drops a second record with a MessageId it already stored. Run one host: a second dispatcher can produce the same fact before either patch lands. Unit tests need no Docker. Smoke tests need Cosmos, Kafka, and Playwright, and they skip when Cosmos or Kafka is not answering.