Managing state transitions in .NET applications — especially in domain-heavy, event-driven, or workflow-oriented systems — can quickly become complex and error-prone. Let’s break down the key challenges:

  • Scattered Logic. In many real-world applications, state-related logic (e.g., when an order can be shipped or canceled) is scattered across services, handlers, and controllers. This leads to:
    • Difficulty tracing business rules;
    • Increased risk of breaking logic when requirements change;
    • A steep learning curve for onboarding new developers.
  • Lack of Structure. While .NET is a powerful platform, it lacks a native FSM (Finite State Machine) library. Developers often rely on if-else chains, switch statements, or even multiple methods and flags. This can cause:
    • Duplicated logic;
    • Inconsistent state behavior;
    • Hidden bugs that are difficult to debug.
  • Difficult Testing. Without a clean abstraction, testing state behavior often means spinning up full services or mocking multiple dependencies. A well-designed FSM can make transition logic easy to test and reason about.
  • Concurrency Issues. In multi-threaded or asynchronous environments (like background workers or async pipelines), state transitions must be thread-safe to prevent race conditions or unpredictable behavior.
  • Missing Auditability. Enterprise applications often need to track state changes — when and why transitions occur. Ad-hoc solutions don’t make this easy to implement or maintain.

Why I Built FlowLite 🔥

As a backend developer, I often encountered systems with complex state behavior - such as order processing, approval chains, and multi-step workflows. Existing solutions were either too limited or too heavy. I wanted something that:
✅Felt idiomatic to .NET;
✅Provided compile-time safety;
✅Avoided reflection or dynamic proxies;
✅Was easy to test, debug, and observe.
That’s why I built FlowLite - a modern, focused FSM engine that combines performance with clarity.

How FlowLite helps 💡

FlowLite addresses these challenges with:

  • Flexible configuration via a fluent builder pattern;
  • Asynchronous and thread-safe transition execution;
  • Custom logic hooks (on state change, entry (entity), or transition failure);
  • Persistence support via JSON or in-memory storage;
  • A lightweight, fast, developer-friendly, and testable core. FlowLite provides a clear, composable, and observable way to manage state transitions without the boilerplate. It’s built with real-world usage in mind and focuses on developer experience and maintainability.

Key Features

Lightweight & Fast — Optimized for high-performance applications;
Asynchronous & Thread-Safe — Ensures smooth execution in concurrent environments;
Declarative API — Define state transitions with a clean and fluent syntax;
State Persistence — Supports JSON-based storage, in-memory storage;
Event Hooks — Capture and log every state change for debugging and auditing (check state, check entry, delete entry);
Cycle Prevention — Detects and prevents cyclic transitions;
Error Handling — Graceful fallback mechanisms for invalid transitions (internal logs);
Final States — Define terminal states where transitions are restricted;
Flexible Configuration — Supports builder pattern for easy transition setup;
Parallel & Sequential State Execution — Suitable for multi-threaded workflows;
Custom Transition Conditions — Add business logic to control transitions;
Dependency Injection Support — Seamless integration into DI containers;
Storage-Agnostic — Plug in your own storage strategy;
Diagram Export — Export transitions as Mermaid.js or Graphviz DOT.

Getting Started with FlowLite 🚀

You can install FlowLite via the .NET CLI or NuGet Package Manager using the link. The FSM engine works in all kinds of projects: Console Apps, Web APIs, microservices, etc.

Define your state and trigger enums

public enum OrderState
{
    Pending = 0,
    Created = 1,
    Shipped = 2,
    Canceled = 3,
    Completed = 4,
    Failed = 5,
    Deleted = 6
}

public enum OrderTrigger
{
    Create = 0,
    Ship = 1,
    Cancel = 2,
    Complete = 3,
    Fail = 4,
    Delete = 5
}

Create the entity class

public class Order
{
    public required int Id { get; set; }
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
    public string AddressLine1 { get; set; } = string.Empty;
    public required string City { get; set; }
    public required string Postalcode { get; set; }
    public required string Country { get; set; }
    public required decimal TotalAmount { get; set; }
    public required string Status { get; set; }
    public string TransitionHistory { get; set; } = string.Empty;
}

Create the state diagram

We’ll use a simple Mermaid diagram to illustrate the state logic:

stateDiagram-v2
    Pending --> Created: Create
    Created --> Shipped: Ship
    Shipped --> Completed: Complete
    Shipped --> Canceled: Cancel
    Canceled --> Failed: Cancel
    Canceled --> Failed: Fail
    Failed --> Deleted: Delete

Image description

Configuring State Transitions

FlowLite supports two storage types:

  • JSON (ideal for development and review)
  • In-memory (recommended for production) You can register it in your IServiceCollection:
services.AddFlowLiteStorage<OrderState, int, Order>(
    StorageType.Memory,
    "C:\\Experiments\\FlowLite\\"
);

Define transition logic using FlowTransitionBuilder

var stateFlowConfig = new FlowTransitionBuilder<OrderState, OrderTrigger, Order>()
    .AddTransition(OrderState.Pending, OrderTrigger.Create, OrderState.Created, async (_, ctx) =>
    {
        ctx.Entity!.Status = OrderState.Created.ToString();
        ctx.Entity!.TransitionHistory += " -> Created";
        await Task.CompletedTask;
    })
    .AddTransition(OrderState.Created, OrderTrigger.Ship, OrderState.Shipped, async (_, ctx) =>
    {
        ctx.Entity!.Status = OrderState.Shipped.ToString();
        ctx.Entity!.TransitionHistory += " -> Shipped";
        await Task.CompletedTask;
    })
    .AddTransition(OrderState.Shipped, OrderTrigger.Complete, OrderState.Completed, async (moveTo, ctx) =>
    {
        if (ctx.Entity!.Id % 2 != 0)
        {
            ctx.Entity!.Status = OrderState.Completed.ToString();
            ctx.Entity!.TransitionHistory += " -> Completed";
        }
        else
        {
            await moveTo(OrderState.Shipped, OrderTrigger.Cancel);
        }
    })
    .AsFinal()
    .AddTransition(OrderState.Shipped, OrderTrigger.Cancel, OrderState.Canceled, async (_, ctx) =>
    {
        ctx.Entity!.Status = OrderState.Canceled.ToString();
        ctx.Entity!.TransitionHistory += " -> Canceled";
        await Task.CompletedTask;
    })
    .AddTransition(OrderState.Canceled, OrderTrigger.Cancel, OrderState.Failed, async (_, ctx) =>
    {
        ctx.Entity!.Status = OrderState.Failed.ToString();
        ctx.Entity!.TransitionHistory += " -> Failed (via Cancel)";
        await Task.CompletedTask;
    })
    .AddTransition(OrderState.Canceled, OrderTrigger.Fail, OrderState.Failed, async (_, ctx) =>
    {
        ctx.Entity!.Status = OrderState.Failed.ToString();
        ctx.Entity!.TransitionHistory += " -> Failed (via Fail)";
        await Task.CompletedTask;
    })
    .AddTransition(OrderState.Failed, OrderTrigger.Delete, OrderState.Deleted, async (_, ctx) =>
    {
        await orderApiClient.DeleteOrderAsync(ctx.Entity!.Id);
        ctx.MarkForDeletion();
    })
    .AsFinal()
    .Build();

🔍 What’s Happening?

  • .AddTransition(...) defines a trigger and what should happen;
  • ctx(context) gives access to the entity (possible to change properties);
  • moveTo(...) allows calling other transitions dynamically (e.g. rerouting on error);
  • .AsFinal() prevents further transitions from that state;
  • .Build() compiles and validates all transitions (cycle-safe).

Now you are ready to call FireAsync(trigger).

What Happens When You Call FireAsync(trigger) in FlowLite 🔥

The method FireAsync(trigger) is the primary way to tell FlowLite. But what actually happens under the hood?

🧭 Step-by-Step Execution

Let’s walk through the full process:

1. Triggering the Transition
You start by calling:

await fsm.FireAsync(OrderTrigger.Ship);

Here:

  • fsm is your state machine instance (StateFlowMachine);
  • OrderTrigger.Ship is the trigger that you want to fire.

FlowLite now prepares to execute the corresponding transition based on your current state and trigger.

2. Locating the Current State
FlowLite looks at the entity’s current state (e.g., Created) and retrieves the corresponding configuration block from the internal transition map.

It checks:

  • Is this state known?
  • Are there transitions registered for this trigger?

3. Validating the Transition
Once a valid transition is found, FlowLite:

  • Verifies any guard conditions;
  • Ensures the transition is currently allowed;
  • Confirms that no final state has been reached (i.e., you can still transition).

If the transition is invalid (e.g., trying to Ship an order in Pending), the execution stop and skip here.

4. Executing the Transition
If the transition is valid:

  • OnExit: Any logic associated with the current state’s exit is executed;
  • Transition Logic: The core action of the transition is performed;
  • This is where you update your entity, call services, or even trigger internal transitions.

Each of these actions can be:

  • Asynchronous (i.e., return a Task or ValueTask)

5. Updating State
After all actions are completed successfully:

  • The entity’s state is updated (e.g., CreatedShipped)
  • The transition history is recorded internally (for diagnostics or testing)
  • The transition result is stored (accessible via fsm.CurrentState, fsm.CurrentEntity, etc.)

If an exception occurs during the transition, the process is halted, and — if enabled — rollback logic may be invoked in future versions (see roadmap).

Example Flow 🧪

Given the finite state machine:

Pending → Created → Shipped → Completed

Calling:

await fsm.FireAsync(OrderTrigger.Ship);

Would:

  • Check that the current state is Created
  • Validate that Ship is allowed;
  • Execute the transition logic (e.g., update the Order.Status);
  • Move the entity to the Shipped state.

Calling FireAsync(trigger) in FlowLite means:

  • Validating the transition is allowed.
  • Executing optional exit logic from the current state.
  • Performing the main transition logic.
  • Entering the new state with optional entry logic.
  • Updating the entity and FSM context.

It’s asynchronous, safe, extensible, and clear — with minimal effort on your side.

FlowLite.Diagnostics

FlowLite.Diagnostics is an optional extension that adds logging, telemetry, and observability to your FSMs. Configure it in your IServiceCollection before creating FSM instances:

services.AddFlowLiteDiagnostics<OrderState, OrderTrigger, int, Order>(opt =>
{
    opt.EnableGlobalDiagnostics = true;
    opt.Telemetry.Enabled = true;
    opt.Logging.Enabled = true;
    opt.Logging.UseConsole = true;
    opt.Logging.UseLogger = true;
    opt.Logging.LoggerFactory = services
        .BuildServiceProvider()
        .GetRequiredService<ILoggerFactory>();
    opt.DiagnosticObserver.Enabled = true;
});

What you get:

  • Console logs of transitions;
  • Diagnostic events;
  • Compatibility with OpenTelemetry metrics via System.Diagnostics.Metrics;
  • Global tracking of all state flow instances for specific state, trigger, entry type. You do not apply diagnostics configuration to every FlowLite FSM instance; If you’re building workflows in production — this is a must-have.

FlowLite.Testing

FlowLite.Testing is a minimalistic and fluent testing library designed for writing expressive and structured unit tests for your FlowLite FSM workflows.

Key Testing Features

✅ Trigger Assertions

fsm.Should().Allow(OrderTrigger.Pay);
fsm.Should().NotAllow(OrderTrigger.Cancel);

✅ State Assertions

fsm.Should().BeIn(OrderState.Paid);

✅ Entity Assertions

fsm.Should().NotNullEntity();
fsm.Should().HaveEntity(order =>
  {
    Assert.Equal("Shipped", order?.Status);
  });

✅ Log Assertions

fsm.Should().Logs().ContainFinalStateLog();
fsm.Should().Logs().Log(LogLevel.Warning, "invalid transition");

✅ History Assertions

fsm.Should().History()
            .StartWith(OrderState.Created)
            .HaveInHistory(OrderState.Created, OrderState.Paid, OrderState.Shipped)
            .Contains(OrderTrigger.Pay, OrderState.Paid)
            .HaveLength(3)
            .ContainsTrigger(OrderTrigger.Ship);

✅ Trigger-Only DSL

fsm.Assert().Allow(OrderTrigger.Pay);
fsm.Assert().NotAllow(OrderTrigger.Cancel);

FlowLite.Testing makes regression-proof testing easy and declarative — a key feature for large systems.

FlowLite.Diag

FlowLite.Diag is a cross-platform .NET CLI tool for visualizing finite state machine (FSM) state transitions based on FlowTransitionBuilder code from the FlowLite library.

Example usage:

flowlite-diag --source "C:\Orders\" --format "mermaid"

Output:

stateDiagram-v2
    Pending --> Created: Create
    Created --> Shipped: Ship
    Shipped --> Completed: Complete
    Shipped --> Canceled: Cancel
    Canceled --> Failed: Cancel
    Canceled --> Failed: Fail
    Failed --> Deleted: Delete

Export to Mermaid or DOT format and paste it into documentation, wikis, or visualization tools like Graphviz. This simple diagram becomes a powerful visualization tool — especially when integrated with FlowLite.Diag to automatically generate diagrams from your codebase.

When to Use FlowLite?

✅ Use FlowLite if:

  • Your system relies on clearly defined state transitions;
  • You want explicit and testable business rules;
  • You care about observability and developer experience;
  • You want to avoid hidden state bugs in large teams. ❌ Avoid FlowLite if:
  • You only need simple event routing (FlowLite is not an event bus);
  • You require dynamic, runtime-defined states (FlowLite uses strong typing).

Roadmap

As part of future development, FlowLite to introduce the planned features:
🔌 ASP.NET Core Middleware integration for state orchestration, IoT management;
🧭 Undo/Rollback support for failed transitions;
🔁 Sub-state machine support (hierarchical FSMs).

Conclusion 🎯

State management in .NET doesn’t have to be painful, scattered, or hard to maintain. With FlowLite, you get a clean and composable way to model, control, and observe state transitions with full type-safety, testability, and performance in mind.If you care about clean architecture, business logic clarity, and maintainable code — FlowLite might be exactly what your project needs. The Github repository is available with the link.