Reasons great engineers struggle to design well

In a recent post, Sean Goedecke argues that you can’t design software you don’t work on. His point is that it’s not possible to come up with good designs without a deep understanding of your existing codebase. That’s fundamentally correct but it’s a necessary rather than a sufficient condition. What I’ve observed is that many engineers who are deep in the code still often struggle to come up with robust designs even if they fully understand what it takes in principle. I think there are a couple of reasons for that:

  • Bias towards tangible risks — The common thinking goes that changing existing code is risky, and therefore as a rule all changes should be minimized. That ethos pushes you towards incrementalism: pick the design that changes the smallest number of lines of code. This can result in broken abstractions that stick around well beyond their useful life. The telltale signs are data models and interfaces with a large number of tacked on parameters that make it difficult to form a coherent mental model. Underlying that is an undifferentiated perception of risk. Breaking the next prod release is a real and tangible risk, while the risk of product velocity grinding to a halt next year as a result of bad design choices is also real but less tangible. If you’re deep in the code you will tend to bias towards the former.

  • Bias towards short-term speed — Effective engineers tend to be very goal-driven and will seek the shortest path to shipping a fix. Changing a design that’s found to be broken is almost always more work than putting in a point solution that works around it. Fast is obviously better than slow, but an accumulation of short-term fixes instead of design adaptations is also what slows you down in the long run. To be clear, this is often not the engineer’s fault: Startup culture strongly (and correctly) rewards fast execution. But the startups that turn into generational businesses are also the ones who are keenly aware of the short-term vs long-term tradeoff and know how to navigate it.

  • Anchoring — If you are deep in the code, your starting point will be the existing code. This can lead you to jump to a solution too quickly and anchor on it prematurely. Strong engineers are generally aware of the shortcomings of existing code and will often have concrete solutions in the back of their minds already, even before deciding to work on a problem in earnest. This can keep them from exploring the design space more comprehensively.

  • Shallow domain knowledge — Deep understanding of the domain is obviously important to modeling the domain correctly. But you can often get pretty far without being a domain expert by relying on pattern recognition. You’ve seen a similar problem before and reach for the solutions that worked last time around. But the pattern can sometimes be mismatched in subtle ways which you won’t notice if you don’t understand the domain well enough.

The key lesson here is to learn to step away from the code at times. Being deep in the code is fundamentally a strength not a liability — problems only come up when you can't leave it. Engineers who consistently produce good designs aren't the ones who abandon code-level thinking for architecture diagrams. They're the ones who can move fluidly between the two, zooming out to see whether the design as a whole still makes sense, then zooming back in to judge the cost and risks involved with a given design. I believe that fluency is a skill you can build deliberately over time, not just a by-product of experience. But it’s also not something you can learn in the abstract. You get better at design by doing more designs and wrestling with the tradeoffs.

Previous
Previous

Agents will further entrench the incumbents in quantitative trading

Next
Next

Figma challengers