”If you want to succeed, get extremely good at designing interfaces.” That was the advice I got when I asked my manager at an internship how to move up the ranks from junior to senior to staff engineer. “Every other skill,” he said, “will soon follow.” His advice contradicted my idea that a great senior dev should learn better communication or management. When asked why, his reasoning was simple. A junior dev might handle the interface between a few methods of a class—the choice of parameters, their names, and return values. As an engineer moves up the ranks, they become responsible for progressively larger interfaces—entire classes within a service, the API between services, the public facing API, and even widely distributed SDKs. High quality interfaces mean people will appreciate your work, approach you for help, and trust you with larger tasks.
I’ve since doubled down on perfecting the craft of interface design, and, as a result, a plethora of related code cleanliness principles appeared. A different mentor of mine recommended I read A Philosophy of Software Design by John Ousterhout. In researching the book, I came across a talk Ousterhout gave at Google about this philosophy:
He claims the most important concept in all of computer science is problem decomposition—breaking down a complex task into pieces that can be built relatively independently. Maintaining independence between components can be a monumental task, if the interfaces between them are poorly constructed. Dependencies begin to stretch across boundaries between components, building tech debt. Eventually, developer velocity slows to a crawl as the system loses its modularity.
Designing the best interface is a challenging and sometimes impossible task in a world of ever-changing requirements. However, spending just a few extra minutes can get you most of the way there without significantly slowing down your productivity. Often I’ll start by asking myself a few questions to ensure I create a robust interface. What other parts of the system rely on this component? How might this change over time as the project grows? How can I unit test this independently? How can I change the internals of the component without affecting other parts of the codebase? These questions can be a great litmus test for robustness, but they don’t offer the entire suite of benefits a well-designed interface can provide.
The best interfaces will take into account its most important end users: other developers. And since these developers are human (for now), we can draw inspiration from traditional design fields to learn how to best suit their behaviors. I have much to learn in this field, but I’ve found Don Norman’s The Design of Everyday Things to be great start. Norman covers many aspects of human-centered design in his book, but most examples tie into his major design principles: discoverability, affordances, signifiers, constraints, mappings, feedback, and conceptual models. In the context of software development, these principles have very real applications.
Think about what happens every time the developer hits the “.” after an object name and the IDE suggests possible autocompletions (shown above). The method names, signatures, and docstrings all influence the conceptual model formed in a user’s mind. Great product designs require no manual, and similarly, great interfaces need no documentation. Imagine having to read a manual on how to use a coffee mug.
All in all, interface design is closely tied to countless other programming principles like information hiding and abstraction. Still, I’ve found that extensive care in crafting interfaces allows the software to “write itself.” Once you’ve outlined the basic structure for a part of your system, all that’s left is to string together the lines of code that implement its functionality—a mindless task for many veteran programmers.
p.s. I’ve created a new project that lets you write stories with strangers, I’d love if you checked it out: strand.jinay.dev
Great read, this is something for us to all reflect on.
- Michael Sheen