Walk-up Usable Codebases
The term “Walk-up usable” (sometimes called walk-up-and-use) describes a system that is so intuitive, a new user can immediately be effective.
An example of something that’s incredible walk-up usable is an elevator. You see a bunch of buttons with numbers on them and it’s fairly obvious what they do.
You probably didn’t get trained to use an elevator. On the other end of the spectrum, if you were dropped into the cockpit of an airplane, you’d probably wish you had a lot of training ahead of time.
When I think about developer experiences, I think about the walk-up usability of the codebase. How long does it take a new developer to become effective? Are the abstractions that exist intuitive? If you’ve ever taken on a large legacy codebase, it probably felt way more like being dropped into the cockpit of an airplane and asked to fly then walking into an elevator and being asked to go to floor 3.
For companies that create libraries or external facing APIs, the same principles apply. A poorly documented and confusing API is a great way to send a potential customer to your competitors.
Designing for walk-up usability
So how can we design for walk-up usability? There are certainly a lot of great books on how to write clean code or use the right design patterns. And at a company level, you can make sure your engineers have time to tackle tech debt that accumulates over time.
For me, one of the most important principles is:
Make it hard or impossible for future developers to make mistakes.
Even the best developers are limited by the mistakes they make.
While having good tests in place can help you catch and address issues quickly, let’s look at an example in Typescript where we can proactively prevent mistakes with our interface design. Say we are a creating a type for API responses:
interface ApiResponse<T> {
loading: boolean,
data?: T,
error?: string,
}
This seems fine, but the question to ask is, “How can someone accidentally misuse this?” Well, they can forget to check loading or if there’s an error. To prevent this, we can re-write this class to use a union type like this:
enum ApiResponseStatus {
Loading,
Success,
Error,
}
interface StillLoadingApiResponse {
status: ApiResponseStatus.Loading,
}
interface SuccessfulApiResponse<T> {
status: ApiResponseStatus.Success,
data: T,
}
interface ErrorApiResponse {
status: ApiResponseStatus.Error,
error: string,
}
type ApiResponse<T> = StillLoadingApiResponse | SuccessfulApiResponse<T> | ErrorApiResponse;
How does this prevent us from making mistakes? If you take the first example and use autocomplete on an ApiResponse you see this:
Which we can contrast with the second example:
In the first example, we are free to use the data before loading is complete. However, in the second example, the only thing we can see at first is the status because that’s the only property shared across our three types. We are not only forced to check the status—we will also get an error if we try to access data without checking the status first:
With this change, we’ve proactively nudged future developers (or our future selves) in the right direction. If you apply this same thinking style to your codebase or the external APIs you build, you can make sure that people depending on your code can be more effective out of the gate.