Systems thinking
Stop looking at lines of code and start seeing flows — inputs, outputs, state, and side effects.
- Describe any component in terms of its inputs, outputs, state, and side effects
- Distinguish a pure transformation from a side effect, and explain why it matters
- Reason about a change by tracing how it ripples through a system
A beginner reads code line by line. Someone thinking in systems reads it as flows: data coming in, being transformed, going out — and the lasting marks the program leaves on the world along the way. Once you can see the flows, large codebases stop being intimidating walls of text and start being maps you can navigate.
The four questions
For any unit of a system — a function, a service, a whole application — you can understand most of what matters by answering four questions:
- Inputs: What does it receive? (Arguments, requests, events, files.)
- Outputs: What does it produce and hand back? (Return values, responses.)
- State: What does it remember between uses? (Stored data, accumulated values, the contents of a database.)
- Side effects: What does it change in the outside world? (Writes a file, sends an email, charges a card, prints to the screen.)
A function that takes numbers and returns their sum has inputs and outputs and nothing else — give it the same inputs and you always get the same output. A function that also logs to a file or updates a counter has a side effect: it reaches out and changes something beyond its return value.
Pure transformations vs side effects
This distinction is one of the most useful in all of programming.
A pure transformation is a function whose output depends only on its inputs, and which changes nothing else. It's easy to test (just check output for input), easy to reason about (no surprises), and easy to reuse (no hidden dependencies).
A side effect is how a program actually does anything observable — without side effects, a program just heats up the CPU. But side effects are also where the danger lives: order suddenly matters, tests get harder, and two parts of the system can interfere through shared state.
The systems-thinking move is not "avoid side effects" — it's push them to the edges. Keep a large, pure core that's easy to reason about, and concentrate the messy, effectful parts in a thin, obvious shell. You'll see this idea again in architecture, testing, and concurrency.
Ask of any function: "If I call this twice with the same input, do I get the same result and leave the world unchanged?" If yes, it's pure — cheap to trust. If no, find the side effect; that's where the care is needed.
Tracing a change through a system
Systems thinking pays off most when something needs to change. Instead of editing the first line that looks relevant, you trace:
- Find the boundary where the new input enters.
- Follow the flow through each transformation to where output leaves.
- Note the state and side effects touched along the way — these are where surprises hide.
- Ask what else reads that same state. A change is rarely local if it touches shared state; that's the ripple.
This is also exactly how you review a change an AI agent proposes. The agent gives you a diff; you give it systems thinking — what flows does this touch, and what else depends on the state it changes?
Check your understanding
Knowledge check: systems thinking
- 1.Which function is pure?
- 2.Which of these are side effects?
- 3.A good strategy is to keep a large pure core and push side effects to the edges of the system.
Where to go next
With flows in view, the next questions are structural: how do we draw the lines between components so that change stays local? That's abstraction and interfaces, and then software architecture.