All complexities should—if possible—be buried out of sight. — David J. Wheeler
Application Programming Interface. Three words that today have been reduced to a simple acronym. We say “API” over and over without stopping to think about what it means. We are, after all, programming applications. But what about the interface?
It’s interfaces all the way down.
Developers are always using interfaces, whether theyʼre provided by our programming language of choice, a framework, or a library a colleague just wrote. Interfaces are the bread and butter of application programming. We don’t just use them, we create them, using other interfaces as building blocks. It’s interfaces all the way down.
In his book The Stack, Benjamin Bratton defines an interface as “any point of contact between two complex systems that governs the conditions of exchange between those systems.” That doesn’t sound like a simple task that can be left to chance or automation—the very use of the word “governing” raises so many questions. What does the interface allow, and what does it forbid? For whom? How is this information being communicated? How do we learn its rules? Moreover, where can we find answers to these questions?
As programmers, we use the word “design” loosely, rarely getting into its implications. But if we dig into design traditions, we can uncover ways to transform the interfaces—the APIs—we build.
We’re coding to solve problems much bigger than our own. The question is, are we properly designing our solutions?
In Designing Design, Kenya Hara writes, “The essence of design lies in the process of discovering a problem shared by many people and trying to solve it.” Design is social: We design to solve problems for many. This also reflects the way we work as programmers, writing code to automate tasks done by many and creating abstractions to avoid repetition. Unless we’re truly doing it just for fun, we’re coding to solve problems much bigger than our own. The question is, are we properly designing our solutions?
Design approaches problems by asking questions, like those raised about the concept of “governing” earlier. If we approach programming problems from a design perspective, as we move toward a solution, we’ll ask questions that not only help us better understand the problem, but also reshape the way we think about the problem itself.
Christopher Alexander introduces the idea of “fitness” in his book Notes on the Synthesis of Form. “Every design problem begins with an effort to achieve fitness between two entities: the form in question and its context,” he writes. Together, form and context are “an ensemble.”
In our case, the context is the scope of our project. Working on a company’s internal API, for instance, is different from working on the API of a database used by hundreds of thousands of developers around the world in mission-critical situations. Context not only matters, it’s paramount to achieving fitness. In the case of large, critical systems, even if the code style looks outdated, you might want to refrain from introducing design changes that could break compatibility. Think twice before converting the 0s and 1s in that config file to booleans—you don’t want to be the reason an ops person gets an alarm at 3 a.m. because someone’s config parser depends on finding integers and not actual boolean values. While correct types should be the norm, they won’t always fit the context in which they’re used.
If the context is the project’s scope, then the form is our API. To best curate our ensemble, we need to ask what context our API designs must fit into. As developers, we design in the context of a programming language, and likely inside a subcommunity of that language. If we probe further and explore what forces introduce friction to the forms we create, we might acknowledge that a Java-inspired, design pattern–heavy implementation, while correct for some contexts, might not fit well into, say, a promise-based TypeScript library.
API design is less about camelCase versus snake_case, or exceptions versus error codes, or async functions versus promise-based functions, and more about the context in which those functions and methods are going to be used. Design is not about what we like the most, but about finding a solution that fits the current context.
In Designing Design, Kenya Hara explains that design is a social activity that must put others first and our egos last. Otherwise, it’s art, an activity in which our individual will is expressed to society at large. At least in an industrial setting, we need to design with others in mind. That brings us to empathy.
Iconic industrial designer Charles Eames introduced the idea of the guest-host relationship in 1972. This generative metaphor induces us to consider a design problem, be it a new product or a software API, in terms of one thought-provoking question: How do you treat people when you invite them into your home? A good host, Eames said, tries to anticipate the needs of their future guests in order to make them feel truly welcome.
Imagine how we might apply this mindset to building APIs. How welcoming are they? How well do they anticipate the needs of future users? Do users need to find workarounds in order to get things done? Consider the word “workaround” for a second—its very composition tells us it’s something a user doesn’t want to deal with. Let’s use an ORM as an example. Sometimes we want to circumvent the object mapping and go directly to the database for a very specific query that its OOP API won’t accomodate. Either the ORM provides a way to build SQL queries, or we have to circumvent it completely for that very specific use case. Such methods show how the designers, our hosts, anticipated our needs while designing the API.
This is why writing tests for our own APIs can give us a sense of how others use them. It helps us develop empathy toward the consumers of the interfaces we’ve created. Might it get annoying to call
database.initialize for the seventh time? Perhaps, with a bit of a re-design, we can find a way to skip that step.
If we design with empathy, we’ll decide to add features based on how people will use the API, rather than on the latest software fad. We’ll understand why, for example, even though we learned to love APIs that use
Maybe in Haskell, they’re not necessarily the best fit in the context of Java. We’d be forcing our users to introduce a coding style to their programs that won’t match the rest. Empathy will drive the fitness of our APIs.
Keep in mind that we’re not designing to judge people’s coding practices, but to help them solve problems. In Design for the Real World, Victor Papanek writes, “Design as a problem-solving activity can never, by definition, yield the one right answer.”
Designs are not perfect, they are perfectible.
If fitness of form depends on context, then as context changes, the fitness of a design changes, for better or for worse. Designs are not perfect, they are perfectible. That is, they can be improved over time. Form in software is more akin to water adapting to a vase than to the vase itself.
Whenever a designed object appears, it changes the world around it. People adapt their behavior to the new object, and some old customs—ones that may have informed the design of the object in the first place—fade away. Designs become outdated in this new world they’ve created. To stay in touch with that world, they must continually evolve. Luckily for us, software is, at least in some contexts, easily malleable.
In his book Designing Japan, Kenya Hara writes that in Japanese folk craft, “the forms of common tools and utensils [are] to be found in the gradual accumulation of experience in daily life over long periods of time.” He underscores that “behavior gives tools the inevitability of their shapes.”
While we can’t wait an eternity to find the right interface shape for our objects, we must understand that any API we design can and should be improved over time. Putting our interface out there, in the hands of our users, will result in feedback that shows us what needs to be improved. Refactoring comes into play here: It’s not just about cleaning up old code or removing duplicated code, but about maintaining fitness and keeping our object interfaces up to date as our users’ needs evolve. They’re the ones who will provide the feedback that helps us improve the “rightness” of our designs.
A design needs to communicate its intent and purpose clearly. In our case, an API must tell its users what it can do, what it can’t do, which operations are safe, and which are dangerous to perform. APIs that require confirmation or extra steps when performing destructive operations are a good example. Think about having to add a
--force to a
git push. It just feels like we’re doing something we shouldn’t be doing, doesn’t it? We know force-pushing in Git isn’t good practice; we’re altering history. Even as interfaces enable us to do things, they should also tell us what things we shouldn’t be doing.
An API’s capabilities must be explicit; we shouldn’t have to guess how to use them. Much as it’s frustrating not to know whether a door should be pushed or pulled, APIs that communicate the wrong affordances will be frustrating and disappointing for users.
In their book Vision in Design: A Guidebook for Innovators, Paul Hekkert and Matthijs van Dijk explain that affordances are clues an object gives us about how it can be used. But, as with fitness and empathy, affordances vary according to each individual’s capacities and competencies. Hekkert and van Dijk note that a ceiling, for instance, affords humans the ability to hang things like light fixtures from it, but does not afford us a place to walk, as it does for a fly.
Once again, empathy matters: It helps us understand that what looks like an affordance to us may not look like one to someone else. As Hekkert and van Dijk write, “What is the purpose of a pen for someone who has lost his hands? Is it still a ‘tool for writing’?”
In the case of code, we have at least two tools that can help us communicate how our objects are supposed to be used: type systems and tests. With types, we narrow the expected set of arguments our API accepts, and with tests, we provide examples of usage.
Hara proposed the idea of “re-design” as a way to reimagine the design of ordinary objects in order to “[make] the ordinary unknown.” He asked his colleagues to “re-design” everyday objects, from toilet paper to passport entry stamps. In doing so, they learned these ubiquitous objects could be much improved—improving storage space in the case of toilet paper, or creating more welcoming entry stamps for visitors to Japan.
By asking questions like the ones we explored at the beginning of this article, we can do the same to revitalize API design. We can explore new areas and gain perspectives that help us improve what we do. Our objects will communicate intent and warn users of potential mistakes, and even get them to reframe their problem and approach. We can examine the context in which weʼre producing these objects, knowing our solution is but one among many. Users will determine its rightness and reshape it as they use it. Fed by empathy, our API designs will become APIs our users can design with, and will evolve.
“Re-design,” as Hara defines it, is about rediscovering design by making the ordinary unknown. By viewing our work with fresh eyes and asking more questions than we provide answers, we can create APIs that empower our users to become designers themselves.
Alexander, Christopher. Notes on the Synthesis of Form. Harvard University Press, 1971.
Bratton, Benjamin H. The Stack: On Software and Sovereignty. The MIT Press, 2015.
Hara, Kenya. Designing Design. Lars Müller Publishers, 2014.
Hara, Kenya. Designing Japan. Lars Müller Publishers, 2019.
Hekkert, Paul and van Dijk, Matthijs. Vision in Design: A Guidebook for Innovators. BIS Publishers, 2011.
King, Simon and Chang, Kuen. Understanding Industrial Design. O’Reilly Media Inc., 2016.
Papanek, Victor. Design for the Real World. Academy Chicago Publishers, 1971.