Date: 2024-03-08 | business | create | software-engineering | tech |
DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
Type-safe programming languages are better than dynamic for building and maintaining software - especially at scale. This is a perpetual point of contention in the software community. But it shouldn't be - type-safe languages scale and dynamic languages don't for the most common and impactful bottleneck in software: the humans building it.
In this post we'll explore the differences between type-safe and dynamic programming languages and how they scale through the lens of a metaphor: wire management.
To explain how type-safe and dynamic languages work in the real world, we're going to use a metaphor that hopefully most people have dealt with before: wire management.

To start, we're going to posit that you can represent a program as a series of wires.
For our own sanity we are going to assume that data only moves through a wire / control flow in one direction. This is mostly true in both domains from a high-level but saves us a lot of complex edges that lead to domain desynchronization. As a sanity check, factory games like Satisfactory, Mindustry, and Factorio all mostly do one-way traversals but complex control flows are still buildable so shouldn't break metaphor too much.
So in our metaphor, a wire represents data / control flows in our program:
Caveat - Known Limitation: The biggest issue with this metaphor is that wires IRL are mostly static (set once, use a lot) whereas the process of software development has quite a bit more change involved. I will try to point this out as we go but keep in mind that modeling software dev in real life is akin to a building that is constantly being renovated in some way.
Recall that all wires look and feel exactly the same in our fantasy world. This raises some obvious issues like how do we ensure we don't cross our wires.
This is the core issue at play between dynamic and type-safe languages so this is a good question to keep in mind. Before we go into scenarios I first want to point out a few common strategies each uses to avoid this so we have shared context.

Dynamic Wires
With Dynamic Wires each wire looks exactly the same - same color, same connectors. This means you could connect a wire carrying electricity to one carrying water - there's nothing stopping you (or often warning you) from doing such a thing.
Common patterns for avoiding wire crossing in dynamic languages:
Type-safe Wires
So all wires in wire-land do look and feel exactly the same but I never said one couldn't upgrade wires to allow for additional info / context on them. This is essentially what Type-safe Wires do.
Each Type-Safe Wire can be:
After reading these descriptions you might say:
And I would say:
Onto some scenarios.
Let's say we've got a pretty standard software engineer desk we need to wire:
This nets us with a pretty manageable ~7 wires. Just plug it all in and see what the results look like.

Using Dynamic Wires this is pretty easy to do. This is like wiring your desk without cable management. With this many wires, we might have a bit of a wire ball behind the desk but still pretty easy to move / switch out when you need to with just a few minutes of tracing wires to see which goes where.

Using Type-Safe wires is like using cable management from the jump. It probably took you a little longer to setup cause you had to make sure you had the right kinds of wires, you took the time to bundle and organize them, and you stored them nicely behind / under the desk. The flip side is that the next time you need to move / update a wire you'll know the exact one you need to change so save yourself the tracing.
Outcome: Draw, maybe slight win for Dynamic w few changes, slight win for Type-safe w more changes.
Both perform about the same at this very small scale. Type-safe wires takes a bit longer to setup so we could even say that Dynamic wires win in this case for time-to-initial-setup.
Recall though that software is constantly changing so this is a desk that is regularly receiving changes about once per day. At this low change velocity we won't see too much of a difference but perhaps the cord management approach is a bit less annoying day in and day out.
Here we've got a little office floor with several units each with:
Recall that software is constantly changing so in Wire land this means that Wires are changing too. For our metaphor we will say that the offices are constantly being renovated (while they're being worked in - joy) with 10 engineers making about 10 changes each day - adding wires, removing wires, changing wires etc.
Now that we're in a more complex wiring scenario, we start getting wires that should not be crossed. If they do bad things happen:
So we don't want to do the wrong thing but we also can't stop changing the wires cause #business - the renovations must go on!

It is at this scale that we start to see Dynamic wires (and languages) start to stretch a bit at the seams. Doing what we did at desk-size worked okay but as we got more engineers changing things at once it got harder to remember what wires were doing what (cause some might have changed from yesterday!).
So we decide to implement a few things to try and prevent this because we're getting fires and nasty stuff and that makes us / our customer sad.

With type-safety we avoid all the fires and nasties altogether. We skip directly to the type guard phase but with a version that:
Of course we're still making 10 changes a day but most of these are changes we choose to do - not urgent fires and nasties we need to put out that prevent forward progress.
The one issue might be that changes sometimes take longer to make - you realize you don't have the correct color wire where you need it and thus need to do more wiring before you call a project "done". But interestingly the time to finish a project (i.e. start -> works correctly including all firefighting and re-work) is much lower cause there is far less re-work cause you did it right the first time.
Outcome: Type-Safe Wires by a long shot.
We can see that Dynamic Wires slowly started bandaiding itself with solutions to try and solve the issue of not understanding what's in the wire because it cost so much in the form of firefighting and rework. They were able to approximate versions of what Type-Safe wires provide - like preventing bad inputs entirely and easily understanding what the wire contains - but were just not able to fully catch up to what a Type-Safe native wire provides.
This may seem too fantastical and abstract but this happens in programming all the time. Dynamic languages may seem like a speed boost at the start but always (and I mean ALWAYS) ends up being a speed hindrance sooner rather than later when individual engineers cannot keep an up-to-date mental model of the full coebase due to size and change velocity. This typically becomes obvious at around 2 teams / 10 engineers and gets worse from there.
We've already been through the main differences in practice between Dynamic and Type-safe Wires. What I want to point out by going a step further in scale is that these problems only increase in magnitude.
We are not even at a large company / piece of software yet and yet we are running into issues with the biggest bottleneck in software development: humans.

There has to be a better way to prevent engineers from making small, unintentional, and catastrophic mistakes without needing them to understand the entire blueprint (impossible) or learning our history of custom fixes (inefficient) or learning through trial by fire (dangerous, demoralizing).
If only we had a wire that was easy to understand by anybody, immune to crossing, and could scale to the next hundred changesPerDay / Engineers / Office buildings.

Outcome: Type-safe Wires are the only ones that work.
I went through several drafts of this post trying to come up with accurate, relatable metaphors that could be ~objectively used to compare dynamic and type-safe approaches in the wild. I think I ended up a bit hot on dynamic but truly it is a dumpster fire at scale so maybe this is appropriate.
I'd love to know if this was helpful / relatable or if this was wrong / confusing. That can help me iterate on making it more understandable for the future.
Also let me know if you have ideas / suggestions for other concepts you'd like explored.
If you liked this post you might also like:
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.