From Async Flow to Documentation: Modeling .NET Microservices with MassTransit Sagas, Mermaid Diagrams, and EventCatalog
Modern microservices are powered by asynchronous flows — messages pass between services, events trigger chains of reactions, and no single system owns the full picture. While this makes systems scalable and fault-tolerant, it also makes them hard to reason about.
Yes, tools like OpenTelemetry, Grafana, and dashboards can help you trace these flows — but only after something has run. You still need to stitch everything together from logs and traces, which often means a lot of digging just to answer: "What should happen here?"
Sometimes you need clarity before things go live. You want to understand how a process is designed to work — who publishes what, who handles it, and what happens next — without waiting for an error to guide you into an observability dashboard.
In this post, I’ll show you how to bring visibility into your system from the start:
-
Model your async workflows using MassTransit’s saga state machines
-
Auto-generate Mermaid diagrams to visualize the logic
-
Embed those diagrams into EventCatalog for living documentation your whole team can access
If you’ve ever stared at a message queue trying to piece together the story, this post is for you.
What Are Saga State Machines?
In a distributed system, a single business process — like placing an order — often spans multiple services: billing, inventory, shipping, notifications. Each service does its part by sending and receiving messages asynchronously.
But that introduces a big challenge:
How do you keep track of the full process? Who ensures each step happens in the right order? And how do you recover if something fails?
That’s where saga state machines come in — and they’re all about orchestration.
A saga is a pattern for managing long-running, multi-step business processes. A saga state machine, specifically, acts as the orchestrator: it tracks the state of a process, handles incoming messages, and sends commands to other services to advance the flow.
In .NET, the MassTransit library gives you powerful tools to define sagas using state machines. You define:
-
States — like
OrderSubmitted
,PendingPayment
,Shipped
-
Events — messages that trigger transitions between states
-
Behaviors — what to do during those transitions (e.g. send a command or publish an event)
Think of it as a workflow engine embedded in your system — one that listens to messages and drives the process forward with clear, testable logic.
In the next section, we’ll look at how to model a simple saga state machine in .NET using MassTransit.
Modeling a Saga in .NET with MassTransit
Let’s say we’re building an e-commerce platform. When a customer places an order, we want to:
-
Wait for payment to complete
-
Then trigger shipping
This is a perfect use case for a saga state machine. We'll define:
-
States:
Submitted
,Paid
,Completed
-
Events:
OrderSubmitted
,PaymentCompleted
,OrderShipped
-
A central saga to orchestrate it all
Step 1: Define the Events
public record OrderSubmitted(Guid OrderId, DateTime Timestamp);
public record PaymentCompleted(Guid OrderId, DateTime Timestamp);
public record OrderShipped(Guid OrderId, DateTime Timestamp);
These are the messages our saga will consume.
Step 2: Define the State
public class OrderState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; } = null!;
public DateTime OrderSubmittedAt{ get; set; }
public DateTime? OrderPayedAt{ get; set; }
public DateTime? OrderShippedAt{ get; set; }
}
MassTransit uses CorrelationId
to tie incoming events to a specific saga instance. The CurrentState
tracks where we are in the process.
Step 3: Build the State Machine
public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
public State Submitted { get; private set; }
public State Paid { get; private set; }
public State Completed { get; private set; }
public Event<OrderSubmitted> OrderSubmittedEvent { get; private set; }
public Event<PaymentCompleted> PaymentCompletedEvent { get; private set; }
public Event<OrderShipped> OrderShippedEvent { get; private set; }
public OrderStateMachine()
{
InstanceState(x => x.CurrentState);
Event(() => OrderSubmittedEvent, x => x.CorrelateById(ctx => ctx.Message.OrderId));
Event(() => PaymentCompletedEvent, x => x.CorrelateById(ctx => ctx.Message.OrderId));
Event(() => OrderShippedEvent, x => x.CorrelateById(ctx => ctx.Message.OrderId));
Initially(
When(OrderSubmittedEvent)
.Then(context =>
{
context.Saga.OrderSubmittedAt = context.Message.Timestamp;
})
.TransitionTo(Submitted));
During(Submitted,
When(PaymentCompletedEvent)
.Then(context =>
{
context.Saga.OrderPayedAt = context.Message.Timestamp;
})
.TransitionTo(Paid));
During(Paid,
When(OrderShippedEvent)
.Then(context =>
{
context.Saga.OrderShippedAt = context.Message.Timestamp;
})
.TransitionTo(Completed)
.Finalize());
}
}
⚠️ Disclaimer
The saga above is a simplified example meant to demonstrate the concept and the overall flow. It skips over important concerns like timeouts, compensations, and message validation.
If you’re looking to learn how to build robust saga state machines in production, I highly recommend checking out the MassTransit documentation.
Visualizing the Saga with Mermaid
Writing the saga is just one part of the story. Understanding how it behaves — especially when collaborating with other teams — is where things often break down.
Fortunately, MassTransit makes it easy to visualize your saga state machine as a Mermaid diagram. This gives you a clear, shareable way to see how your workflow behaves: what states exist, which messages trigger transitions, and how the process flows.
Exporting a Mermaid Diagram from MassTransit
To generate Mermaid diagrams from your saga, you’ll need to install the following NuGet package:
dotnet add package MassTransit.StateMachineVisualizer
This nuget provides a helper to generate the diagram:
var generator = new StateMachineGraphVizGenerator<OrderState>();
string mermaid = generator.CreateMermaidStateDiagram(new OrderStateMachine());
Console.WriteLine(mermaid);
This will output a Mermaid-compatible diagram definition like this:
flowchart TB;
0(["Initial"]) --> 4["OrderSubmittedEvent«OrderSubmitted»"];
1(["Submitted"]) --> 5["PaymentCompletedEvent«PaymentCompleted»"];
2(["Paid"]) --> 6["OrderShippedEvent«OrderShipped»"];
4["OrderSubmittedEvent«OrderSubmitted»"] --> 1(["Submitted"]);
5["PaymentCompletedEvent«PaymentCompleted»"] --> 2(["Paid"]);
6["OrderShippedEvent«OrderShipped»"] --> 3(["Completed"]);
That when rendered with mermaid looks like this:
flowchart TB;
0(["Initial"]) --> 4["OrderSubmittedEvent«OrderSubmitted»"];
1(["Submitted"]) --> 5["PaymentCompletedEvent«PaymentCompleted»"];
2(["Paid"]) --> 6["OrderShippedEvent«OrderShipped»"];
4["OrderSubmittedEvent«OrderSubmitted»"] --> 1(["Submitted"]);
5["PaymentCompletedEvent«PaymentCompleted»"] --> 2(["Paid"]);
6["OrderShippedEvent«OrderShipped»"] --> 3(["Completed"]);
This is a simple diagram, but it already helps you see the full lifecycle of your workflow at a glance.
In more complex sagas with branching, timeouts, and compensations, this visualization becomes even more valuable.
Documenting the Flow in EventCatalog
Writing a saga and visualizing it is powerful, but the real magic happens when you share that understanding with the rest of your team. That’s where EventCatalog comes in.
EventCatalog is an open-source tool designed to help you document and explore event-driven systems. It’s Markdown-based, easy to version alongside your code, and supports Mermaid diagrams out of the box.
By embedding your saga’s Mermaid diagram into EventCatalog, you give your team a single source of truth that’s:
-
Version-controlled with your code
-
Always up to date
-
Accessible to developers, architects, and PMs alike
How to Embed Your Diagram
Once you’ve generated the Mermaid diagram using MassTransit, just drop it into EventCatalog, it natively supports mermaid.
You can place this diagram in an event file, a service file, or a domain overview — whatever makes the most sense for your team.
EventCatalog also lets you document:
-
Who publishes and subscribes to each event
-
JSON schemas
-
Event timelines and use cases
Why This Matters
In distributed systems, visibility is everything. When workflows span multiple services and messages, understanding what’s supposed to happen becomes just as important as knowing what did happen.
Yes, tools like OpenTelemetry, Grafana, and Application Insights help you monitor your system. You can trace requests, visualize spans, and debug issues when something goes wrong — but only if the system is running and only after a request has already gone through.
Sometimes, though, you just need to answer:
-
What is this service supposed to do when it receives this event?
-
What happens after this step completes?
-
Where does this flow end?
These are design-time questions, and they shouldn’t require log diving or dashboard spelunking.
That’s where the combination of:
-
MassTransit saga state machines (to model orchestration)
-
Mermaid diagrams (to visualize it)
-
EventCatalog (to document and share it)
...becomes incredibly valuable.
It gives your team:
-
Clarity before runtime — You can understand the flow before you run the system.
-
Source-controlled documentation — Diagrams live in your repo and update as code changes.
-
Cross-team alignment — Devs, testers, and PMs can all view the same diagrams and understand the system’s behavior without digging into code or telemetry data.
Rather than relying solely on observability to react to problems, you now have proactive visibility into how your system is designed to work.
Conclusion
Asynchronous workflows are the backbone of modern microservices — but they can quickly become a tangled mess of events, handlers, and unknowns.
In this post, we walked through a clean, practical way to bring clarity to those flows by:
-
Modeling orchestration with MassTransit saga state machines
-
Generating Mermaid diagrams to visualize the state transitions
-
Embedding everything into EventCatalog for discoverable, living documentation
This approach gives your team visibility before runtime, aligns devs and stakeholders around shared flows, and reduces the mental overhead of working in event-driven systems.
Observability tools like OpenTelemetry are powerful when things go wrong, but this gives you understanding before anything breaks.
Check out the full working example here:
https://github.com/GeorgopoulosGiannis/blog.asyncflows
It includes:
-
A functioning MassTransit saga in .NET
-
Mermaid diagram generation
-
EventCatalog setup with documented events, commands, and flow
Feel free to fork it, extend it, and use it as a foundation for documenting your own microservices.