Swift Concurrency in legacy code: where migrating pays off — and where it doesn't
The line does not run between old and new, but between a value and a stream: actors where state is shared, Combine and RxSwift where it keeps flowing.
· 9 min
Andrey Gershengoren · · 9 min
The interesting question about an automation does not come up on the day it is built. It comes up six months later, when it needs to change. Either that is a twenty-minute adjustment, or it is the sentence "please do not touch it, it works" followed by a rebuild.
What decides between those two outcomes is not the complexity of the workflow, and not the tool either. It is a list of obligations that were met or were not.
Up front, because this text reads wrongly otherwise: n8n is fit for production. A visual workflow is not inferior code, it is code in a different notation, with the same obligations.
The problem comes from the attitude the tool invites. "It is only an automation" leads to nobody asking the questions one asks about a service as a matter of course. A script that triggers payroll runs for five years is not a script any more, however it came about.
Demo and production do not differ in scale. They differ by the list below.
The first question for any workflow is what happens when a step does not work. The external service answers with a server error, the model runs into a timeout, the webhook arrives twice.
In a throwaway workflow the answer is: it stops and tells nobody. In a maintainable one there are retries with growing intervals, an error path for when retrying does not help, and somewhere to put what could not be processed at all.
The last point is skipped most often. Without such a place, failed items simply disappear, and afterwards nobody can say how many there were.
The question that most reliably reveals whether someone has production experience: what happens if this workflow runs twice on the same data?
This is not a theoretical case. Webhooks get delivered twice, retries fire after a timeout that actually succeeded, somebody re-runs an execution by hand. If the answer is "then the email goes out twice" or "then the record is in the CRM twice", the workflow is not production-ready, no matter how clean the graph looks.
The safeguard is rarely elaborate: a business key checked before writing, or a write that has no effect the second time. What is elaborate is retrofitting it once the duplicate records are already there.
A workflow that fails at night has to be able to say what happened. That means output at the places that will matter later, and a notification on failure.
The opposite is not "no logging". It looks more concrete than that: the failure is noticed a week later because somebody complains, and then reconstruction begins. Between those two states lies no question of technology, only the decision whether anyone is meant to notice.
Changes to a live workflow with no way back are the normal state of many installations. This is where tinkering and engineering separate most clearly, because what is missing here is not knowledge but discipline.
What belongs to it is well known and unspectacular: export into version control, a second environment to try things in, and changes run there before they go live. The effort is small and the return is invisible as long as nothing happens. Which is exactly why it gets left out.
I consider this the most honest test on the whole list. The other four can be forgotten while building. This one is left out deliberately.
When a function node contains two hundred lines of JavaScript, the right answer is not to structure them better. It is that this logic does not belong there.
An orchestration tool connects services. It is not a place for application logic, because what application logic needs is missing there: tests, types, debugging with tools that step expression by expression. Logic of that size belongs in a service the workflow calls.
Knowing where the glue ends is the difference between someone operating a tool and someone deploying one.
As soon as a language model sits in the graph, the previous assumptions no longer fully hold. Every other node is predictable in principle: same input, same output, and when it fails, it fails visibly.
A model node fails quietly. It delivers something that looks like a result, and the workflow keeps processing it, because nothing told it otherwise.
In practice one rule follows: a model's output is not a record until it has been checked. The check sits behind the node and answers at least two questions. Does the answer have the shape the next step expects, meaning structure, fields, types. And does its content make sense in domain terms, measured against what may legitimately come out at this point.
What happens next is a decision, not an accident: try again, hand it to the error path, or put it in front of a person. Without that check the error travels onward and becomes a number in a report that nobody questions any more.
The price is build time, and it falls entirely at the start. A workflow with error paths, an idempotency check and control points takes considerably longer than the same workflow without. Both look identical at the end, as long as everything works.
There is friction in daily use too: going through a second environment means nothing gets changed in two minutes any more. That is the real reason this point is so often skipped, and it is understandable.
So whoever pays is paying for something they do not see at first. It becomes visible at the first change request. A throwaway workflow is cheaper to build and more expensive to own, because every change turns into a rebuild, and a rebuild costs building again.
Not every automation has to satisfy all of this, and claiming otherwise would be an ideology rather than a way of deciding.
A one-off data migration runs once and is deleted afterwards. A personal helper has exactly one user, who notices every fault immediately. An experiment for a week is meant to answer a question and then disappear. In all three cases the effort is not justified.
The difference is not in the result but in the intent. Maturity is not building everything solidly. It is deciding deliberately what you are building.
It becomes a problem only when the experiment turns into a production system without a decision. That rarely happens by resolution. It happens because nobody makes one.
The five points are not a quality standard but a list of questions to answer before building: what happens on failure. What happens on a double run. Who learns about an outage. How do I get back. And where does this tool's responsibility end.
Whoever has answered them builds an automation that can be changed. Whoever never asked builds one that will have to be replaced, and does not know it yet.
The line does not run between old and new, but between a value and a stream: actors where state is shared, Combine and RxSwift where it keeps flowing.
· 9 min
Generated code is rarely sloppy, it is over-engineered. That barely shows up in a diff, which is why my checkpoint sits before the first line of code.
· 8 min
You describe the situation, I tell you whether and how I can help. No slides, no sales pitch.