Article content
There is a moment in PLC programming that you never really forget. You have a new program loaded on the controller, the machine is idling at its home position, and you press the enable key. At that instant, physics becomes software. Conveyor belts turn. Cylinders extend. Sensors fire. If your code has a logic error, you find out not with a stack trace in a terminal but with a metal arm moving in the wrong direction at full speed. You learn, quickly, to think differently about what it means for code to run.
I spent the better part of two years in Stuttgart working with industrial PLCs — programming Siemens S7 systems, wiring sensor arrays, calibrating servo drives, writing ladder logic for assembly-line sequences. At the time I saw it purely as mechanical work: make the thing move, make it stop, make it do it again ten thousand times without fault. It was only after I transitioned into web development — first vanilla JavaScript, then React, then distributed backend systems — that I realised how much those years on the factory floor had quietly shaped the way I write software. The parallels are not superficial.
The scan cycle: time as a constraint, not an abstraction
A PLC does not run code the way a general-purpose computer does. It does not fire events, respond to interrupts, or juggle threads. It executes a single loop — the scan cycle — from top to bottom, over and over, typically every 5 to 20 milliseconds. In each pass, it reads all input states, evaluates every rung of logic, and writes all output states. That's it. Every cycle. Without exception.
This is called determinism, and it is the founding principle of industrial control. The machine does not care about network latency, garbage collection pauses, or an operating system scheduler deciding something else is more important right now. The scan cycle will complete in a known, bounded time, or the watchdog timer will fault the CPU and stop the machine. There is no partial execution, no "it was running slow that day." The system is predictable by design, because unpredictability in a factory kills people.
You internalise this constraint. You stop writing code that assumes "it'll probably be fast enough" and start writing code that proves the timing. Every branch matters. Every conditional that could extend a scan cycle is examined. It is perhaps the most focused I have ever been about computational cost — not because I was being academically rigorous, but because the alternative was a fault alarm at 3 AM.
Ladder logic and the first state machine
PLC programs are traditionally written in ladder logic: a graphical language that looks like an electrical relay schematic turned into code. Contacts (inputs) and coils (outputs) are arranged in rungs. A rung evaluates left to right, and the coil at the end is energised if the logical path through the contacts is complete. It is, at its core, a domain-specific language for expressing boolean state transitions over time.
Consider a simple sequence: a motor that should run only when a start button is pressed, a safety guard is closed, and no emergency stop is active. In ladder logic it looks like this:
Ladder Logic — Siemens STL-equivalent rungs
// Rung 1: Start latch — motor runs until E-stop or guard opens |--[ START_PB ]--+--[ MOTOR_RUN ]--+--[/E_STOP]--[GUARD_CLOSED]--( MOTOR_RUN )--| | | | | +--[ MOTOR_RUN ]--+ // Rung 2: Fault output — any safety condition triggers alarm |--[/E_STOP]--( FAULT_LAMP )--| |--[/GUARD_CLOSED]--( FAULT_LAMP )--|
What you are writing, though you may not call it that, is a state machine. MOTOR_RUN is a boolean state. Transitions into and out of it are guarded by conditions. The latch rung (using the motor's own output as a holding contact) is a canonical implementation of a self-sealing circuit — once started, it sustains its own condition until an external event breaks it. That pattern is, structurally, identical to what you write in Redux when you describe a loading state that persists until a success or error action arrives.
Here is the same logic in Python, stripped to its essence:
Python — equivalent state transition
from dataclasses import dataclass, replace from typing import Literal type MotorState = Literal["idle", "running", "fault"] @dataclass(frozen=True) class SystemInputs: start_pb: bool e_stop_ok: bool guard_closed: bool def next_state(state: MotorState, inputs: SystemInputs) -> MotorState: if not inputs.e_stop_ok or not inputs.guard_closed: return "fault" if state == "running": return "running" # self-seal: sustain until safety clears if inputs.start_pb: return "running" return "idle"
The structure is the same: pure function, no side effects in the transition itself, outputs determined entirely by current state plus inputs. The ladder rung is a reducer. I did not know the word "reducer" in Stuttgart. But I had been writing them for two years.
What it actually taught me about failure
Web software fails gracefully, or at least that is the goal. A 500 error is annoying; a retry usually fixes it; a user loses a few seconds. Industrial control software has a different relationship with failure. A dropped packet from a fieldbus sensor means a robot arm is operating blind. A logic error that allows two interlocked cylinders to extend simultaneously can destroy tooling worth more than a year's salary. Failure modes are not edge cases you think about after the feature works. They are the first thing you design around.
The discipline this creates is hard to replicate artificially. When you have spent time staring at a wiring diagram asking "what happens to this output if this input wire comes loose?", you develop an instinct for examining the unhappy path first. What is the safe state? What does the system do when it loses signal, not when everything is nominal? This is the question I now ask habitually when designing APIs or distributed systems, because I learned the hard way that the nominal case is not the one that will break production at 2 AM.
The surprising mirror: event loops, async state machines, React
When I first read about JavaScript's event loop — a single thread, processing one task at a time from a queue, non-blocking I/O handled via callbacks — I had an immediate sense of recognition. Not intellectually. Something closer to muscle memory. The structure was the same: a controlled, sequential evaluation loop over a known state, driven by external signals. The signals were user events and network responses rather than digital inputs from a sensor array, but the model was isomorphic.
React's state model deepened the parallel. A component's state at time t is a pure function of its state at time t−1 and the action dispatched — exactly the next_state function above, except the "scan cycle" is a render. The virtual DOM is the PLC's output image: a snapshot of what the world should look like, computed fresh each cycle, applied atomically. I have seen React developers surprised by stale closure bugs in useEffect — they are confused because they expect time to be continuous. PLC thinking immediately reframes it: you are reading the state snapshot from the previous cycle. Of course the value is stale. Your input image was captured at the start of this scan.
The virtual DOM is the PLC's output image: a snapshot of what the world should look like, computed fresh each cycle, applied atomically.
XState, the JavaScript state machine library, might as well have been written by a controls engineer. Explicit states. Guarded transitions. Entry and exit actions. When I first read its documentation I felt, for a moment, like I had been teleported back to a STEP 7 project in Stuttgart. The vocabulary was different. The formalisation was the same.
The mental shift: from deploy-and-done to continuous deployment
There is one profound discontinuity between industrial control and web software, and it took me longer to absorb than I expected. A PLC program, once commissioned and signed off, is essentially frozen. You do not push a patch to a running machine mid-production. You schedule a maintenance window. You test the new version in a staging environment — often a physical replica of the production line. You validate it exhaustively. Then, during a controlled shutdown, you load the new program and restart. The deploy is an event. It is planned, witnessed, and recorded.
Continuous deployment — where a merge to main can push code to millions of users within minutes — was, initially, genuinely alarming to me. The concept of a feature flag as an incremental toggle, the idea of rolling deployments that update 5% of nodes at a time, the normalisation of "we'll monitor and rollback if needed" — these felt, from a controls background, like a reckless abdication of the pre-commission testing discipline I had been trained to take seriously.
Over time I came to see that the web's answer to this is not recklessness but a different kind of rigour. Observability — metrics, distributed tracing, structured logging — is the monitoring panel of the production line. Canary deployments are the functional test run at reduced throughput before full cutover. Feature flags are the manual override that lets you de-energise a rung without modifying the program. The principles map. The tooling is completely different. Learning to trust the tooling took a deliberate shift of instinct.
Where the skills diverge: concurrency and non-determinism
The place where PLC thinking actively misleads you is concurrency. A scan cycle is, by definition, single-threaded. There are no race conditions in a PLC because there is only one thread of execution and it always runs to completion before the next scan begins. The mental model is clean, simple, and completely wrong for distributed systems.
When I first encountered database transaction isolation levels, I found them strange and over-engineered. When I first debugged a race condition in a Node.js service — two concurrent requests both reading a counter, both incrementing it, both writing it back — I was genuinely confused. This does not happen on the factory floor. It cannot happen, by design. Understanding that concurrency is the central problem of networked systems, not a pathological edge case, required unlearning the comfortable determinism of scan-cycle thinking.
The same applies to eventual consistency. A PLC knows, with certainty, the state of every input at the start of every scan. A distributed system often does not know the current state of a node that has not reported in 200 milliseconds. Is it down? Is it slow? Is the network partition between you and it, or between it and the outside world? The answer is: you do not know, and your system must function reasonably across all three possibilities. There is no PLC equivalent of this. The factory floor assumption — that the fieldbus is either working or tripped — is a luxury that distributed systems do not enjoy.
Why constraints breed clarity
The deeper lesson I took from industrial automation is not about any specific technical pattern. It is about what constrained environments do to your thinking. When a mistake in your code can be physically dangerous, you stop tolerating ambiguity in requirements. When your program runs on a CPU with 512 KB of memory and a 10 ms scan budget, you stop writing code that "should be fine." When the person who will maintain your program is an electrician reading a ladder diagram, not a developer reading TypeScript, you stop writing clever code.
Software engineering has largely solved the physical safety problem — a bug in a web app does not injure anyone. But it has not solved the discipline problem. The constraints that made PLC programming rigorous were external and unavoidable. In web development, the analogous rigour is optional. It has to be chosen. I am grateful I got to spend time in an environment where it was not optional. It made the choice easier.
I still think in scan cycles sometimes. When I am reviewing a React component and something feels wrong about its state updates, I find myself asking: what is the input image at the start of this render? When I am designing an API endpoint, I ask: what is the safe state if this call never gets a response? The questions are different. The habit of asking them came from a factory floor in Stuttgart, and I do not think I would trade it.