Skip to content

← Archive

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, and 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, and not because the code was difficult. The act of teaching exposed gaps in my own understanding that I hadn't known were there.

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, and if I'd been writing the code myself I'd have separated those concerns without thinking about it at all.

Then we got on a call to discuss it and I froze. "Because it's better" isn't feedback. "Because separation of concerns" just names the principle without explaining it. What I needed was the specific reason the separation mattered here: what would go wrong without it, what it would cost us later, what it would make easier.

That moment taught me that there's a difference between being able to apply a rule and being able to explain why the rule exists.

The stages of understanding
The stages of understanding

How Understanding Develops

Through code reviewing, and through a lot of 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, and your brain starts to pattern-match without anyone ever 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, so the learning stayed 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 and names things well, and if someone asks why, you struggle to get much past "it feels right" or "that's how it should be done." You've internalised the rule without being able to externalise it. Most competent developers sit here for years and it's a perfectly functional place to be.

The jump is when you can apply the rule, explain the principle behind it, describe the contexts where it does and doesn't apply, and generate new instances that illustrate it. You've moved from knowing-how to knowing-that. You can defend it, and, more usefully, you know when to break it.

That jump is what code reviewing forced on me.

The Feedback Loop

I originally had a simpler model in my head. You do first, then you read to understand what you did. Practice, then theory. 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, hit a new edge case, and go back to reading with a sharper question.

Reading without doing produces cargo cult understanding. You can recite the principles and you've never felt the pain they address. You know you should favour composition over inheritance, and you've never been burned by a deep inheritance hierarchy that turned a simple change into a cascade through twelve files.

Doing without reading produces superstition. You know that something works and you might well attribute it to the wrong cause. You always write small functions because a senior once told you to, and you think it's about readability when it's actually about testability. The practice is correct while the mental model underneath it is wrong, and wrong mental models eventually lead you to apply the rule where it doesn't help, or miss it where it would.

The strongest developers alternate between the two quickly. They try something, read about why it worked, try a variation, read a different perspective. 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. Articulating the specific benefit required me to think about it more carefully than writing the code ever had.

When I had to explain why a certain function should be extracted, "it's too long" wasn't going to do. I had to identify the actual reason: this function is doing two things with different rates of change, and when one changes, the other shouldn't have to.

Each of those explanations forced a precision of thought that writing code alone never demanded. The junior's questions were the best forcing function I'd come across, 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 to them is noise, 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, at the point where it has a hook to attach to.

Don't stop at the doing either. A team that only learns through osmosis, watching seniors and picking up habits, will produce capable practitioners who can't explain their decisions, which is fine right up until someone has 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 get questioned rather than assumed.

The Test

Here's a test I now apply to myself. For any practice I follow, can I explain what I do, 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. They didn't teach me new techniques. They made me understand the ones I already had.