A lot of developers learn the SOLID principles through class-heavy examples.
That is probably why the conversation so often gets stuck there.
People start to associate SOLID with inheritance hierarchies, interface forests, service classes, and object-oriented ceremony.
But the useful part is not the ceremony.
It is the design pressure.
One of the simplest ways to apply that pressure in plain TypeScript is this shape:
fn(args, deps)
Where args is the input for this call and deps is the set of collaborators the function needs.
You can read that as: data in, capabilities in.
fn(args, deps) is not a replacement for SOLID. It is a simple function shape that makes several SOLID ideas easier to apply without forcing you into class-heavy design.
Many software systems fail for one very boring reason.
Not because of microservices. Not because of monoliths. Not because of whatever methodology war is trending this week.
They fail because they are unpredictable.
If you make a change and you cannot reliably determine the impact, you cannot safely evolve the system. And when you cannot evolve it, it starts behaving like legacy.
Determinism is the bridge between "works on my machine" and "works every time, everywhere."
fn(args, deps) gets you there — not because it is a clever trick, but because it makes boundaries explicit. Your logic programs to interfaces, which is what lets you control sources of nondeterminism.
Just because two pieces of code look the same does not mean they are the same.
The most common architecture mistake is not too little abstraction. It is too much, too early. You see duplication, you extract a shared module, and six months later that module is a monster held together by special cases and boolean flags.
Dan Abramov gave a talk about this called The Wet Codebase. The core argument: the wrong abstraction is far more expensive than duplication. Once an abstraction exists, it creates inertia. Nobody wants to be the person who suggests copy-paste.
fn(args, deps) changes this calculus. It makes abstractions cheap to create, cheap to test, and cheap to undo.
When a function's deps grow too large, that can be a signal that some responsibility has stabilized into its own function — and that new function itself follows fn(args, deps). (This is basically SRP pressure showing up in your signature; see the SOLID post for that framing.)
Russian dolls. Each layer independently testable. Each layer reversible.
A lot of developers hear "dependency injection" and immediately think of containers, decorators, registration APIs, lifecycle scopes, and framework magic.
That reaction is understandable.
But that association often leads people to overcomplicate a problem that has a much simpler starting point.
At its core, dependency injection just means this:
Pass collaborators in explicitly instead of reaching for them implicitly.
One of the simplest ways to do that in plain TypeScript is this shape (introduced in the series starting point: fn(args, deps)):
fn(args, deps)
Where args is call-specific input and deps is the set of collaborators the function needs.
You can read that as: data in, capabilities in.
fn(args, deps) is flexible enough to support composition, testing, and clean application wiring without forcing you into a DI framework.
AI coding agents produce code faster than you can review and understand it.
One pattern works in both new and legacy codebases because you can adopt it incrementally, without breaking callers.
For business logic, treat every function as having two inputs: data (args) and capabilities (deps).
Without a clear constraint, generated code becomes harder to reason about: dependencies disappear, side effects spread, composition gets messy. This is why visible structure is essential.
You have an Awaitly workflow: a few steps, some dependencies, typed results. It works. When someone asks "what does this do?" or you need to debug a run, you're left tracing through code.
What if you could see the same workflow as a diagram? awaitly-visualizer plugs into your workflow's events and turns them into that picture. For a checkout that runs fetchCart, validateCart, processPayment, then completeOrder, you get output like this:
Same idea as Mermaid flowcharts: steps, order, success and failure. This post walks through adding it step by step. All of the code below lives in a test in the repo so you can run it yourself.