Building Software by Drawing State Diagrams in XState
(And a Touch of Design)

Motivation
I have always wondered what is the most crucial thing for creating effective, endurable, and scalable software systems, following the stories of how some companies have core software written in languages ​​that were “popular” in the 70s of the last century or experiences such as rewriting WinForms 90s Desktop software to adapt to the modern Cloud/Web/Mobile world.
A few months ago, I attended a tech conference in Skopje, WhatTheStack. One of the presentations reminded me of an interesting concept in computer science: State Machines.
At this event, it was presented how UI components can be modeled using Finite State Machines and the selling point for me to try the XState library was the visual editor and my belief that such CASE (Computer Aided Software Engineering) tools can significantly simplify the definition of certain business and system processes in the form of a diagram drawing/wiring that is quite understandable even for non-IT people.
So I decided to try to develop my ATM system to describe all its UI interactions and states using XState! #LearnByDoing

What is a state chart?
State diagrams (or charts) provide an abstract description of a system's behavior. The content described in a state chart is a deterministic finite state machine (FSM), a conceptual model that is defined by the state in which the system currently is, and the events that cause a transition from the current state to another (or the same) known state from the finite number of states.
ATM: System breakdown
Note: To maintain the conciseness of the presentation and the reader's concentration, I highly recommend cloning and starting the project by yourself, available at the link provided at the very end of this post. The project is a React web application and a Node Express (server.js) application.
Creating an initial state
Every ATM will ask you to insert your credit/debit/crypto card, so the initial state of my ATM state machine is: Insert Card. After successfully “inserting” a card, we are presented with a menu where we need to authenticate. This is how the definition of the initial state looks in code:
"Insert Card": {
on: {
CardInserted: "Unauthorized Menu"
}
} // creating a new state with the visual editor is even simpler
In the interest of my time, while developing the application, I didn’t go into some details (such as whether the correct card was inserted).
Interaction with the main application
During the lifecycle of an actor (in this case, an ATM FSM instance), the outside World (in this case, the frontend application itself) can find out the current state of the instance or send an event message with the send function.
{state.matches('Insert Card') && <InsertCard onClick={() => send({ type: "CardInserted" })} />}
Therefore, while we are on the InsertCard screen, some external actor (in this case, onClick) will send an event of type “CardInserted” and will “trigger” the machine to make a transition to the Unauthorized Menu. As simple as that!
What about data?
Each instance also has its global memory, defined as Context, where we can store data needed during the machine’s lifecycle.
In my case, the PIN is stored as userId in the Context of the ATM. Although this is unnecessary, my goal was to create an updateUserId event that triggers a transition within the same state when updating the PIN, which will, at some point, be used as input when calling another type of actor to authorize ourselves – Promise actor.
The Promise actor
Using fromPromise we’ve defined our authorization actor, which essentially makes a POST request to the server and validates the user (the PIN/userId). What is interesting about Promise actors is that they have exactly two outcomes, done and error, which would direct the user to the Authorized Menu or Incorrect Pin (simplification) screen, respectively.
Reusability
What enhances the reusability is using another machine as a child actor in our machine. For example, the ATM uses a countdown machine to transition out from some screens where the user receives an informational message (Thank you for using our services).

In this machine, we have a countdown event that after 1 second transitions to the same state and that event has a guard (it can be executed if the
remaining count seconds != 0). A similar guard iscountingFinished– we can’t finish if theremaining count seconds != 0. XState allows us to call an action upon each entrance of a specific state, andnotifyParentis such an example, upon each entrance in the counting state, notify the parent (ATM instance) about how many seconds are left!
ATM: Visualised
To better visualise how all this would look to the outside world, that is, from the point of view of the ATM user, my colleague Andrej Dojchinovski stepped in and helped design the user interface of our imaginary ATM. In the following section, he’ll share the thoughts and reasoning behind the designs in a few words.
Guides and Constraints
Let’s start by clarifying that the goal was never to invent a new type of ATM, but rather to create a smooth, familiar user flow, guided by UX and UI best practices, with the main objective of simulating the functionalities provided by the state machines working in the background. We decided that our fictional ATM would include a touchscreen, a physical numeric keypad, a card slot, and a cash slot.
UI Design
Since the ATM is purely fictional and unaffiliated with any existing bank, I had the creative freedom over its branding and visuals. Assuming it might be used outdoors in daylight, I gave the interface a bright, neutral background to allow for better contrast and text readability. I used a semantic approach for the colour palette, organising it into three groups:
Teal and mint green for primary, desired actions and success messages
Dark desaturated teal for secondary actions and neutral text
Raspberry pink for cancelling actions and error states
To ensure visual consistency and harmony, I applied shades from these same color groups to all graphic elements and illustrations throughout the designs.
Content, Layout, Actions
The overall digital interface includes two distinct types of screens:
Screens that prompt physical interaction with the ATM (e.g., inserting or removing a card, entering a PIN, or depositing/collecting cash)
Screens that require touchscreen interaction, where the user selects from multiple on-screen options
In addition to differing in expected user input, these two types also vary in content and layout.
The first type is characterised by a clear written instruction accompanied by an image as a visual cue indicating the expected physical action.

Here we see the initial state screen prompting the user to insert their card to start using the ATM, followed by the second screen requiring authentication with a PIN.
The second type presents the user with a set of choices, prompting them to proceed by tapping one of the displayed options. Users can end the session at any point (except during background processes such as checking a balance or counting cash).

After a successful authentication, the user is presented with the main menu, with the option to withdraw cash, deposit cash, and check the card balance. If they choose to withdraw cash, they are given several pre-filled amounts based on commonly requested values, and the option to input a different amount.
To prevent system errors and avoid unnecessary background processes, some actions are disabled until additional steps are completed. Animated screens provide real-time feedback during background operations, reassuring the user that the system is working. When errors occur, users are shown an explanatory message along with suggested steps to resolve the issue.
All of these use cases can be seen in the example below.

Once the user has performed all desired actions and necessary steps, the final screen displays an informational message confirming that the session has ended successfully. This screen is implemented with a countdown timer and automatically transitions back to the ATM's initial state after a short delay.

Prototype
To see all of this in action, check out the full prototype via the following link:
[Hint: Whenever an external action is required (such as inserting a card), simply click anywhere on the screen to proceed in the prototype.]
Final Words
Of course, many other functionalities weren’t even mentioned in this blog post, so I encourage you to try modeling the behavior of some system that comes to your mind.
I believe developing software as a state machine will guarantee its longevity, easier integration into different media, reliable system upgrades, and easier testing/observance of the possible state paths within the system (btw, writing tests for FSM is also fun!).
Finally, thank you for making it this far! Below you can find the GitHub link to the project itself, and if you have any comments and/or suggestions, I encourage you to contact me at gjorgi.krenkov@codechem.com
Till my next blog post,
Best,
Gjorgi Krenkov






