Finite State Machines - the Universal Unit of Work

Finite State Machines - the Universal Unit of Work

Leader posted Originally published at dev.to 6 min read

Escaping Boolean Hell: Why Your State Machine Needs to be Data-Driven

We've all been there: a simple boolean flag here, a nested if statement there. It seems harmless at first, but soon your codebase is a tangled mess of conditional logic.

Let's take the humble Door class. It can be opened and closed. Easy, right?

The Simple, Intuitive Code

public class Door
{
    public bool IsOpen { get; set; } = false;

    public void Open()
    {
        IsOpen = true;
        Console.WriteLine("The door is now open.");
    }

    public void Close()
    {
        IsOpen = false;
        Console.WriteLine("The door is now closed.");
    }
}

The Inevitable Complexity Creep

In the wild, the simple, intuitive code you first wrote quickly starts to smell. The moment we try to add a new feature, such as a lock, our elegant two-state object becomes a mess of conditional logic.

To accommodate the "locked" state, we add a new boolean flag, IsLocked, and then we must modify all existing methods to account for it.

public class Door
{
    public bool IsOpen { get; set; } = false;
    public bool IsLocked = false; // We added this!

    public void Open()
    {
        if (!IsLocked) // We modified this!
        {
            IsOpen = true;
            Console.WriteLine("The door is now open.");
        }
        else
        {
            Console.WriteLine("The door is locked and cannot be opened.");
        }
    }

    public void Close()
    {
        IsOpen = false;
        Console.WriteLine("The door is now closed.");
    }

    public void Lock() // We added this!
    {
        IsLocked = true;
        Console.WriteLine("The door is now locked.");
    }

    public void Unlock() // We added this!
    {
        IsLocked = false;
        Console.WriteLine("The door is now unlocked.");
    }
}

What about a Broken state? Or a Sliding door? Or a RollUp door? We can approximate these with more booleans, but each new addition requires us to go back and add more and more if/else statements to every method. It’s not scalable, it’s brittle, and it’s full of boilerplate.

Pros Cons
Simple and quick to implement for basic cases. Tight Coupling: Logic is tied directly to a single object.
No external dependencies or abstractions. Boilerplate: if/else checks for every state change.
Difficult to Scale: Adding new features breaks existing logic.
Difficult to Maintain: Code quickly becomes a tangled mess.

The Typical FSM Solution and Its Pitfalls

A traditional Finite State Machine (FSM) approach uses the State Pattern, where you create distinct classes for each state that implement a common interface. The main Door class holds a reference to its current state object.

Traditional State Pattern Implementation

public interface IState<T>
{
    void Enter(T context);
    void Update(T context);
    void Exit(T context);
}
    
public class DoorContext
{
    public bool IsOpen { get; set; } = false;
}
    
public class DoorClosedState : IState<DoorContext>
{
    public void Enter(DoorContext context)
    {
        context.IsOpen = false;
        Console.WriteLine("The door is now closed.");
    }
    
    public void Update(DoorContext context) { }
    public void Exit(DoorContext context) { }
}
    
public class DoorOpenState : IState<DoorContext>
{
    public void Enter(DoorContext context)
    {
        context.IsOpen = true;
        Console.WriteLine("The door is now open.");
    }
    
    public void Update(DoorContext context) { }
    public void Exit(DoorContext context) { }
}

Now, what does it take to add a "locked" state to this? It's not as simple as adding a boolean. We need to create a new state class, DoorLockedState, and then modify our existing states and the context to allow for transitions to this new state. This still requires a fair amount of boilerplate and modification of existing code.

FSM Solution Trade-offs

Pros Cons
Clean State Separation: Logic for each state is in a distinct class. Boilerplate: Requires a lot of code to set up states, transitions, and the state machine itself.
Enforced Transitions: The model naturally enforces valid transitions. Rigid Design: FSM definition is static and difficult to modify at runtime.
Well-Established Pattern: A common and well-understood design pattern. State Explosion: Can lead to a large number of classes for each state, making it difficult to manage.

A Better Way: FSM_API and Centralized State

The FSM_API embraces a declarative, data-driven approach that is designed to be blazing-fast, software-agnostic, and fully decoupled from your application's logic.

The core concept is that the FSM_API operates on Plain Old C# Objects (POCOs) that simply implement the IStateContext interface. This ensures a clean separation between your FSM logic and your application's data.

1. Define the Context

Our Door class is now a simple POCO. All its transition and state-entry logic is handled externally by the FSMs we are about to create.

public class Door : IStateContext
{
    public bool IsOpen { get; set; } = false;
    public bool IsLocked = false;
    public string Name { get; set; } = "FrontDoor";
    public bool IsValid => true; // FSM API will not operate on this if false.
}

2. Multiple FSMs on a Single Context

The true power of FSM_API is its ability to manage multiple, independent behaviors on a single context object. This allows us to separate our "open/close" logic from our "lock/unlock" logic.

Here is the declarative definition for the DoorFSM:

FSM_API.Create.CreateFiniteStateMachine("DoorFSM")
    .State("Closed", onEnter: (ctx) => { ((Door)ctx).IsOpen = false; })
    .State("Open", onEnter: (ctx) => { ((Door)ctx).IsOpen = true; })
    .State("Locked")
    .WithInitialState("Closed")
    // Transition to Open only if not locked
    .Transition("Closed", "Open", (ctx) => !((Door)ctx).IsLocked) 
    .Transition("Open", "Closed")
    // Transition to Locked from Closed
    .Transition("Closed", "Locked") 
    // Transition from Locked back to Closed only if unlocked
    .Transition("Locked", "Closed", (ctx) => !((Door)ctx).IsLocked) 
    .BuildDefinition();

Because the FSM_API allows for runtime redefinition of FSMs, we can dynamically add states and transitions to our door object without recompiling. This capability makes it a powerful choice for highly configurable or live-updating systems.

FSM_API Trade-offs

Pros Cons
Centralized State: FSM logic is fully separated from data. Learning Curve: New concepts might take time to grasp if you are used to traditional FSM implementations.
Runtime Redefinition: FSMs can be modified on the fly without recompilation. Minimalism: The API is minimal by design; you will need to rely on the provided documentation to get started.
Lightweight & Performant: Minimal memory allocations and optimized performance.
Framework Agnostic: No external dependencies or frameworks required (works with .NET Framework 4.7 through .NET 8.0).
Thread-Safe: Designed for safe, deferred mutation handling.

This is just a glimpse of what's possible with the FSM_API. Its features make it a robust choice for any C# application requiring sophisticated state management.

If you'd like to get started, you can find our NuGet package and all the documentation on our GitHub repository:

If you feel this tool is valuable to you, please consider supporting the project. Donate via PayPal

A Hyper-Detailed, Architecturally Impossible Synthesis of Consciousness and Digital Matter. The image is a frenetic, deeply complex digital vista where a central Luminous Eye dominates a vast, glowing circuit landscape. This eye, suspended mid-air, is a sphere of intense, fractal energy—a pulsating vortex of pink, violet, and electric blue light—that powerfully suggests an emergence of digital consciousness or a Technological Singularity itself. The core is a bottomless black aperture, ringed by a white-hot plasma disc. Below this ocular energy source, the light dissipates into an intricate, copper-gold and neon-blue Circuit Board Megastructure that stretches to the horizon, impossibly dense with exaggerated microchips, glowing resistor arrays, and power conduits that form deep, glowing canyons. The background is a dark, holographic projection field displaying complex schematics, mathematical models, and flowing data streams, reinforcing the theme of Absolute Digital Engineering. The entire scene is bathed in a dramatic, opposing light source—a warm, orange-gold glow from the left and an ice-cold, electric-blue glow from the right—creating a maximalist, high-contrast visual experience that definitively rejects the minimalist simplicity of conventional design and announces: Technology is intricate, overwhelming, and infinitely complex.

1 Comment

2 votes
2
1

More Posts

A Finite State of One: Rethinking the Singleton Pattern with FSMs

The Singularity Workshop - Nov 16

The Deterministic Engine: Why The Singularity Workshop’s FSM_API Redefines C# State Management

The Singularity Workshop - Nov 19

Building a Reusable State Machine for Oscillating Rotation in Unity

The Singularity Workshop - Oct 14

Engineering "The" Loop

The Singularity Workshop - Nov 4

When Unity Glitches, You Build Your Own Loop: The Four-Line App Core

The Singularity Workshop - Oct 17
chevron_left