The Evolution of Software Complexity
From small scripts to distributed systems — and the maintainability crisis that made functional programming inevitable.
To understand why functional programming exists, it helps to look at the shape of the software we write today and trace how it got that way. The constraints that drove FP into the mainstream were not academic preferences — they were the lived experience of teams drowning in the complexity of their own programs.
This chapter tells that story.
From scripts to systems
Software started small. In the 1970s and 1980s, a typical program
was a single file, written by one person, executed once, then
discarded. State lived in a handful of variables. Side effects were
explicit: a printf here, a file write there. You could fit the
whole thing in your head.
Then the machines connected. Then the users multiplied. Then the programs started talking to each other across networks. Each step added a new dimension of complexity that wasn't there before:
- Concurrency — multiple things happening at once
- Distribution — multiple machines, partial failure
- Asynchrony — operations that take time and can be cancelled
- Persistence — state that outlives the program
- Scale — billions of users, exabytes of data
A modern web service might handle tens of thousands of concurrent requests, each carrying its own thread of mutable state, each touching a database, a cache, a queue, a third-party API. The surface area of a typical program has grown by orders of magnitude in fifty years.
The hidden enemy: mutable state
The single largest source of complexity in software is mutable shared state — values that change over time and are visible from many places at once.
Consider the simplest possible mutable program:
count is a single number. Three calls. Easy. Now imagine ten
modules can call increment, three of them run in parallel, two of
them are inside event handlers that fire whenever the user clicks,
and one of them lives inside a setInterval. Suddenly the question
"what is the value of count right now?" has no single answer.
The behavior of any individual function now depends on the entire history of every other function, in the order they ran, on the threads they ran on. The program is no longer a thing you can read top-to-bottom — it's a four-dimensional cloud of possible execution traces.
This is sometimes called spooky action at a distance: changing a line in module A breaks a test in module Z, and there is no syntactic link between them.
The cost of side effects
A side effect is anything a function does besides return a value: writing to disk, printing to a console, mutating an argument, sending a network request, reading the clock, throwing an exception, updating a global counter.
Side effects are not evil — every useful program has them — but they have costs:
- They make functions hard to test. You can't just assert on a return value; you have to mock the disk, the network, the clock.
- They make functions hard to compose. Two functions that both mutate the same global state can no longer be reordered safely.
- They make functions hard to reason about. Reading the body is not enough; you must also know when the function was called, who called it, and what the rest of the world looked like at the time.
Compare with a pure version:
The second version is longer, but you can reason about it with no external context. The inputs are visible. The outputs are only the return value. There is no hidden channel through which it can surprise you.
The rise of concurrency and asynchronous systems
The hardest bugs in modern software almost all involve time. Two
requests arrive in the wrong order. A setTimeout fires after the
component it was for has unmounted. A Promise resolves into a
closure that captured a stale variable. A database write races
against a read.
Imperative programs handle time by mutating state: the request handler updates a session, the timer fires and re-reads it, the component remounts and re-syncs. Every interaction is a tiny mutual ritual of "look at the world, change the world, hope the world looks right the next time someone looks".
Two clicks. One lost update. No error, no exception, no log line — just a number that is quietly wrong.
Functional programming offers a different deal. Instead of "look at the world, change the world", you say: "given this snapshot of the world, what is the next snapshot?" Time becomes an input, not a shared resource. Two clicks produce two transformations of two snapshots, and the system reconciles them on purpose, not by accident.
Reasoning at scale
When a codebase is small, you can hold its mental model in your head. You know which functions mutate what, which call orders are valid, which states are reachable. As the codebase grows, that mental model becomes too large for any single person — and yet the language itself still assumes a single coherent reasoner.
The classic symptoms of a codebase that has outgrown its style:
- "I'm afraid to rename this function — I don't know who calls it"
- "Every change requires three days of manual QA"
- "The new hire spent two weeks just learning the data flow"
- "We don't refactor — we add code around the bug"
These are not failures of people. They are failures of technique. A language and a style that scaled to 1,000 lines does not automatically scale to 100,000 or 1,000,000.
What FP offers, in one sentence
Functional programming responds to the complexity crisis with a single, radical claim:
If you build your program out of pure functions over immutable values, and you push side effects to the edges, the parts of your program you care most about — the logic — become as easy to reason about as arithmetic.
The rest of this course is about how to actually do that, in TypeScript, for real applications.
The next chapter looks at where these ideas came from — the mathematical and historical roots of functional programming, from lambda calculus through Lisp and Haskell to the modern ecosystem.
Why does mutable shared state make large programs hard to reason about?
It is always slower than immutable data
The behavior of a function depends on the entire history of every other function that touched the same state
TypeScript cannot type-check mutable variables
It always causes the program to crash