Design is a hugely underrated component in programming and engineering. It’s often forgotten, left in the cold by junior (or sloppy) developers, or those who view programming as purely logical.

Design is all about communicating intent. It’s about communicating what’s possible. How, and in which direction, levers can be pulled. It communicates how one action affects another. What’s not working. What’s broken. With consistency, it helps build intuition on how to work with a library, and how to treat and plan for errors.

Poorly designed software suffers from the exact same sets of issues as design in the physical world. Ambiguous labelling. Unclear limitations. Low quality documentation. Over-complicated interfaces. Inconsistent and confusing designs. While the opportunities for automation in software are far more, the barrier to entry is also far lower.

Here’s a couple things to think about.

Abstraction and naming

Thoughtful use of abstraction signals purpose. It shows where, and in which context an object may be used. It also allows for unnecessary complexity to be hidden away in a single, digestible unit.

Abstraction allows for guidance and exposure of what is possible, what isn’t possible. It also adds an opportunity for any parents to define safe limits for functionality.

Designing on purpose

This starts with a mental exercise. You should have clear answers to the questions:

  • Who’s using it? Will it be used in professional settings? Is it part of a complicated stack? Use the context of adjacent projects to help guide this.

  • Why would someone use it?

  • When asking for input, what could go wrong? Am I validating all potential inputs? How can I restrict incorrect formats, and promote correct ones? This is where old techniques like fuzzing[1] come into play.

  • When I show an error, is it helpful? Can the user solve the problem with provided error prompts?

  • When performing work, what could go wrong? How can I plan for it? Is user intervention required? If so, how can I make it as smooth as possible?

  • You should also design your code as though others will be working on it. Are you naming things correctly? Using clear abstractions? Does the function name describe the single thing the function does?

  • Do you offer tunable logging? With useful messages? Are you showing too many messages? Too few?

  • Do your storage schemas reflect the core of the idea? If they can’t, for any reason, does your application logic hide this extra complexity?


The least creative of the points. This one is dead simple: stick to convention. Use PEP8. Use something like black[2] to ensure everyone’s code looks and feels the same. If you have a set of hard-and-fast rules defining how things should be, there’s no ambiguity.

This should be enforced at the IDE and at the repository level. It should also be a pre-requisite when you’re setting up continuous integration.

As engineers, we need to do everything we can to reduce extra cognitive load. This is a dead-simple way to achieve it.

Where to from here

Look at how others use your work, look at how you use others’ work. Read and contribute to open source. Keep up to date with movements and trends (though, beware of noise and hype without substance).

In general, give yourself enough time to think. Sleep on it. Look from other angles. Get feedback. Draw things out. Take time to think, so others don’t have to.