Be eventful, not hierarchical

In today's increasingly complex software environment, the architectures we adopt play a pivotal role in determining how well our applications perform, scale, and maintain. Among the multitude of architectures available, Event-Driven Architecture (EDA) and Hierarchical State Machines (HSM) have been widely utilized in various applications. However, many experts argue that EDA holds an edge over HSM in most cases. This blog aims to explore these reasons with examples in C#, while acknowledging that each has its own strengths and appropriate use-cases.

The Hierarchical State Machine

To begin with, let's understand the basic structure of a hierarchical state machine (HSM). A HSM, in essence, is a finite state machine extended with hierarchy (nested states) and orthogonality (parallel states). A classic example of HSM is a DVD player. There are overarching states like "Off," "On," "Playing," and "Paused," each of which may contain sub-states. This nesting of states brings a lot of clarity in design but it also increases complexity when the system scales up (Samek).

Let's illustrate this with a C# example:

class DvdPlayerPlaying : StateMachine
{
    private readonly IStateMachine? _parent;
    private readonly ILogger _logger;

    private DvdPlayerState? _state;

    public DvdPlayer(ILogger logger, IStateMachine parent = null)
    : base(parent)
    {
        _logger = logger;
        _parent = parent;
    }

    override protected async Task Received(DvdPlayerState state)
    {
        switch (state)
        {
            case State.Off:
                await ExitSubstate();
                break;
            case State.On:
                _logger.Warning("DVD player is already on and has entered the video playing substate.");
                break;
            case State.Pause:
                if (_state == State.Pause) {
                    _logger.Warning("Video is already paused.");
                } else {
                    _state = state;
                }
                break;
            case State.Play:
                if (_state == State.Play) {
                    _logger.Warning("Video is already playing.");
                } else {
                    _state = state;
                }
                break;
        }
    }
}

The state transitions and handling can quickly become hard to maintain when we add more functionality to the DVD player, like a "Rewind" or "Fast Forward" feature.

The Event-Driven Architecture

On the other hand, an Event-Driven Architecture (EDA) responds to actions or changes (events) rather than polling or maintaining the state of the system. Events can be generated by humans, software or even hardware. A system following EDA listens for these events and responds accordingly (Etzion and Niblett).

Let's revisit the DVD player example using an EDA approach in C#:

public class VideoPlayingController : EventSenderSubscriber<DvdPlayerState>
{
    private readonly ILogger _logger;

    public VideoPlayingController(ILogger logger)
    {
        _logger = logger;
    }

    override public async Task OnStateChanged(DvdPlayerState state)
    {
        if (state != State.Play || state != State.Pause) {
            _logger.Warning("VideoPlayingController ignored event because it was not Play or Pause.");
            return;
        }

        await ChangeState(state == State.Pause ? DvdPlayerState.Play : DvdPlayerState.Pause);
    }
}

In the EDA example, the DVD player transitions can easily be maintained using guarding clauses; it is also easy to extend as it triggers an event and any interested component can listen and respond accordingly.

Advantages of EDA

Scalability

As the complexity of a system increases, EDA scales better than HSMs. It's relatively easier to add new events than to manage the transition of states in a complex system (Hoffman). Additionally, since events are decoupled from the components that process them, the system can scale horizontally by adding more processing units.

Flexibility

EDA provides more flexibility since it allows for loosely coupled components. A component just needs to emit an event without worrying about who is going to handle it. This makes it easier to extend the system, as adding new functionality might just mean listening to the existing events (Luckham).

Real-Time Processing

In today's world, where real-time data processing is paramount, EDA holds an edge because it is inherently designed to deal with real-time events (Margolis).

Efficiency

EDA can also be more efficient as it is reactive and doesn't waste resources polling or checking states when there are no changes (Strozzi).

In conclusion, while HSMs might be appropriate for simpler, self-contained systems, as a system grows in complexity, the flexibility, scalability and efficiency provided by an event-driven architecture makes it a more appealing choice.

However, it's worth noting that the choice of architecture should depend on the problem at hand and there is no one-size-fits-all solution. A thoughtful mix of various architectural patterns often leads to the most robust and scalable systems.

Works Cited

Etzion, Opher, and Peter Niblett. Event Processing in Action. Manning Publications Co., 2010.

Hoffman, David. Event-Driven Architecture: How SOA Enables the Real-Time Enterprise. Addison-Wesley Professional, 2019.

Luckham, David C. The Power of Events: An Introduction to Complex Event Processing in Distributed Enterprise Systems. Addison-Wesley Longman Publishing Co., Inc., 2002.

Margolis, Michael. Arduino Cookbook. O'Reilly Media, 2011.

Samek, Miro. Practical UML Statecharts in C/C++. Newnes, 2008.

Strozzi, Carlo. "The NoSQL Movement: LAMP and the open-source tradition." Linux Journal, no. 162, 2007, p. 5.

Previous
Previous

Why use C# over Java?

Next
Next

Let data be data