Engineering Philosophy · Part 2 of 4
Knowing How Isn't Knowing Why
I started code reviewing for a junior developer a few months ago. It taught me more about my own understanding than I expected, and revealed a model of learning that changes how I think about software knowledge.
15 March 2026 6 min read
A few months ago, I started code reviewing for a junior developer on our team. I expected it to be straightforward: read the code, spot the issues, talk them through on a call. I'd been writing production software for a while. How hard could it be to explain what I already knew?
It turned out to be one of the most revealing experiences of my career so far. Not because the code was difficult, but because the act of teaching exposed gaps in my own understanding that I didn't know existed.
The Gap Between Doing and Explaining
There's a specific moment that stays with me. I was reviewing some code where the junior had written a service that mixed data fetching with business logic. I knew instinctively that this was wrong. I could feel it. If I'd been writing the code myself, I would have separated those concerns without thinking. It was automatic.
But when we got on a call to discuss it, I froze. "Because it's better" isn't feedback. "Because separation of concerns" is just naming the principle without explaining it. I needed to articulate the specific reason this separation mattered in this context: what would go wrong if we didn't do it, what it would cost us later, what it would make easier.
That moment taught me something important: there's a difference between being able to apply a rule and being able to explain why the rule exists.
How Understanding Develops
Through code reviewing, and many conversations with my mentor about this exact problem, I've come to think about software knowledge as moving through stages that are easier to describe than to name. The first is learning by accumulation: you watch someone more experienced, you see them make a choice, and later you see them make the same kind of choice in a different context. Your brain starts to pattern-match without anyone stating the rule. This is how my mentor taught me: not by explaining SOLID principles, but by walking through specific instances of applying them in real code. The learning was implicit. Here's how I structured this service, here's why I split this module, here's what I changed.
At some point, you can apply the pattern yourself. You write code that separates concerns, keeps functions small, names things well, but if someone asks why, you struggle to get past "it feels right" or "that's how it should be done." You've internalised the rule but you can't externalise it. This is where most competent developers sit for years, and it's a perfectly functional place to be.
The jump is when you can not only apply the rule, but explain the principle behind it, describe the contexts where it does and doesn't apply, generate new instances that illustrate it. You've moved from knowing-how to knowing-that. You can defend it, and critically, you know when to break it.
That jump is what code reviewing forced on me.
The Feedback Loop
Here's where I originally had a simpler model in my head: you do first, then you read to understand what you did. Practice, then theory. But I've come to think that's too linear.
What actually happens is more like a feedback loop. You see instances and develop tacit knowledge. Then you read something, an article about dependency injection or a chapter on domain-driven design, and it clicks because you've already felt the problem it solves. That reading reshapes how you see the next instance. You apply the refined understanding, encounter a new edge case, go back to reading with a sharper question, and the cycle continues.
Reading without doing produces cargo cult understanding. You can recite the principles but you've never felt the pain they address. You know that "you should favour composition over inheritance" but you've never been burned by a deep inheritance hierarchy that made a simple change cascade through twelve files.
Doing without reading produces superstition. You know that something works, but you might attribute it to the wrong cause. You always write small functions because a senior once told you to, but you think it's about readability when it's actually about testability. The practice is correct but the mental model is wrong; wrong mental models eventually lead you to apply the rule in contexts where it doesn't help, or fail to apply it in contexts where it would.
The strongest developers alternate between the two rapidly. They try something, read about why it worked, try a variation, read a different perspective, and so on. The theory and the practice aren't sequential; they're interleaved, each one sharpening the other.
What Code Reviewing Taught the Reviewer
The irony is that reviewing code for a junior developer pushed me to a deeper understanding of several concepts I thought I already had.
When I had to explain why we inject dependencies rather than instantiate them directly, I realised my own understanding of dependency injection was more mechanical than principled. I knew the pattern, but articulating the specific benefit required me to think about it more carefully than I ever had when just writing the code.
When I had to explain why a certain function should be extracted, I couldn't just say "it's too long." I had to identify the specific reason: this function is doing two things with different rates of change, and when one changes, the other shouldn't have to.
Each of these explanations forced a precision of thought that writing code alone never demanded. The junior's questions were the best forcing function I'd encountered. Not because they were sophisticated, but because they were honest. "Why?" is the most powerful question in software development, and it's the one we stop asking once we reach unconscious competence.
The Implication for How We Teach
This has practical consequences for how learning gets structured in software teams.
Don't start with the textbook. If someone hasn't felt the pain of tightly coupled code, explaining the dependency inversion principle is just noise. It'll sound theoretically correct and practically meaningless. Let them write the tightly coupled code, let them experience the change that cascades everywhere, and then show them the principle. The learning sticks because it has a hook to attach to.
But don't stop at the doing either. A team that only learns through osmosis, watching seniors and picking up habits, will develop capable practitioners who can't explain their decisions. That's fine until they need to make a decision in unfamiliar territory, where there's no pattern to match against. That's when the rationalised understanding matters.
The opportunities for that kind of deeper learning are obvious once you look: code reviews, pair programming where the more experienced person narrates their thinking, architecture decision records where you have to write down why you chose an approach, and team discussions where practices are questioned rather than assumed.
The Test
Here's a test I now apply to myself: for any practice I follow, can I explain not just what I do, but why I do it, and when I would stop doing it?
"I write unit tests" is a what. "Because they let me refactor with confidence" is getting somewhere. "When the cost of maintaining them exceeds the confidence they provide (which happens with highly volatile UI code), I'd stop" is the full thing.
The junior developer doesn't know it, but their code reviews have been a great learning experience for me. Not because they taught me new techniques, but because they forced me to understand the ones I already had.