The Jump From Writing Features to Thinking in Systems
by Eric Hanson, Backend Developer at Clean Systems Consulting
The Ceiling Nobody Labels
There's a ceiling in most engineers' careers that doesn't look like a ceiling from the inside. You're shipping consistently. Your code quality is good. You're trusted with increasingly complex features. And then you get feedback that you're "not quite senior yet" and you can't fully decode what that means.
What it usually means is this: you're still thinking primarily in features, not in systems. You zoom in on the work in front of you with exceptional focus and skill. What you're not yet doing — reliably, automatically — is zooming out to see how the component you're building fits into everything around it, what it does to the system when it changes, and what the system needs from it that the ticket doesn't say.
This is the actual jump. It's not about knowing more things. It's about changing what you look at.
What Feature Thinking Looks Like
Feature thinking is optimized for the unit of work. The question is: how do I build this thing correctly?
You read the requirements. You understand the expected behavior. You write code that implements the behavior. You write tests that verify the behavior. You ship.
This is excellent work. Most software requires it. The problem is that it's incomplete as a way of thinking about complex systems.
Feature thinking treats the inputs and outputs of a component as fixed: the requirements tell you what they are. It treats the system context as a boundary condition, not an object of attention. It focuses on correctness within the defined scope and treats everything outside the scope as someone else's concern.
What Systems Thinking Looks Like
Systems thinking asks a different set of questions before, during, and after building:
Before: What existing parts of the system does this change? What assumptions does it make about their behavior? What assumptions do they make about the behavior of the component I'm building?
During: If this component fails or behaves unexpectedly, what does that do to everything downstream? What data flows through this path and what are its consistency requirements? Is this component observable — can someone understand what it's doing from logs and metrics without reading the code?
After: If requirements change in the most likely directions, which parts of this design need to change? Where are the seams? What would make this hard to modify?
The feature question is "does this work?" The systems question is "what does this do to the system?"
The Concrete Difference
A feature-thinking engineer builds a new background job that sends weekly digest emails to users. The job queries users, filters for those with digest preferences enabled, renders emails, and sends them. It works. All the tests pass.
A systems-thinking engineer builds the same job and also asks:
- What happens when this job runs concurrently with itself? (Distributed lock or uniqueness constraint on sent_at timestamp)
- What happens when the email service is down? (DLQ or retry table)
- How do we know if it's silently failing for some users? (Per-job success metrics and alerting on below-expected send counts)
- What happens at 10x the current user volume? (Batching, pagination, index on the user query)
- How does someone disable this for a specific user in production without a deploy? (Feature flag or per-user suppression)
None of these are in the ticket. All of them are in the job of building a system.
The Practices That Develop Systems Thinking
Own things across their full lifecycle. Building a feature teaches you about implementation. Being on-call for it teaches you about operations. Investigating its first production incident teaches you about failure modes you didn't anticipate. Engineers who only experience the build phase develop feature thinking. Engineers who live with what they built develop systems thinking.
Read post-mortems actively. Not to see what went wrong, but to see what assumptions the original builder made that turned out to be wrong. Every incident is a case study in the gap between the system as designed and the system as it behaves.
Draw the system before you design the component. Before writing any code for a significant feature, draw the existing system and identify where the new component connects. This forces you to see the connections explicitly rather than treating them as context you'll deal with later.
Ask "what breaks" before "how do I build." Make it a ritual. Before writing the first line of implementation code, spend ten minutes listing the ways the thing you're about to build could fail or misbehave. Then design to address the most likely and most costly failures.
The Practical Takeaway
Choose one piece of work you've shipped in the last three months. Map every other system component it depends on and every component that depends on it. For each connection, write down: what happens if that dependency is unavailable? What assumptions does this component make about the data it receives? If you find gaps in your original design, you've just done the work of systems thinking retroactively. The goal is to do it prospectively next time.