Principles of Developer Experience
Welcome to the inaugural post in my new space, which I hope to keep fresh with insightful content. As a front-end engineer and engineering manager, I focused on building developer tools for open source projects such as React Native, Jest, or Metro for the past ten years.
In a hurry? Skip straight ahead to the Principles.
I am restless in searching for a better user experience, and I lose sleep over misaligned pixels. I deeply care about how people feel about the tools they are using every day. This experience informed how I think about building software, and the team I work with lives by several principles we defined together. I am sharing this manifest with you in hopes that it’ll be useful to shape your high-level thinking of developer tooling, too.
Developer Experience or “DevX” is the experience developers have when building, maintaining, testing, deploying, and analyzing software. Think about the tooling you use: Some of them are probably hard to use, slow, and there aren’t any plans to improve them.
Usually, it is not something that organizations get the liberty to focus on: DevX gets de-prioritized, and arguments for investments can be shut down quickly because there is no direct connection between improving DevX and generating business value (💸). This disconnect makes it hard to measure the impact of focusing on DevX. It also means that developers must ruthlessly prioritize any DevX work: how can they bring value to other developers in minimal time? Let’s start by completely reframing how we look at tools:
Internal tooling is rarely intuitive, documented, or fast. If we want to improve our tools, we must change our perspective and look at developer tools as real products. Products should be well-designed, have carefully crafted workflows, and focus on achieving satisfactory outcomes quickly. The best products are delightful.
Let’s think of developer tooling as a means to increase the velocity at which engineers deliver solutions. We can define metrics that can substantially impact user satisfaction or developer velocity: Suppose common repetitive tasks can be completed ten times faster or more efficiently. Developers will ship projects sooner, end up switching context less often, and likely be happier. Productivity is hard to measure, and we’ll approximate metrics in a future post. For now, we’ll focus on building a framework for thinking about DevX.1
I’m going to list principles in order of priority.2 The first principle is the lowest in the stack, meaning it’s the foundation we are least likely to violate. A stack is necessary because when we have to make trade-offs on principles, we’ll always choose to stick with the one lower in the stack. Imagine a stack of books: it’s easy to take off the book on the top, but removing the book on the bottom requires a lot of effort, and every time you do, you end up with a different order, or you may even get rid of some books in your stack. You’ll only want to touch the book on the bottom when absolutely necessary.
The great thing about principles is that once we define them, decisions become more obvious because everything turns into a matter of trade-offs with clear resolution paths. However, as we’ll see in the examples below, it’s painful when we do have to violate a principle. That’s the entire point of principles, though!
Here are the five principles we are going to explore today:
- Focus on the User
- Incremental Migration
- Continue to re-evaluate Assumptions, Constraints, and Trade-Offs
- Maximize Option Value
The most essential principle is to put the user always first. For developer tooling, that means:
- Workflow performance: How fast does it execute everyday tasks?
- Signal: How actionable is the tool’s signal to support the developer in achieving their goal?
- Reliability: How reliable is the tool in completing a task?
- User Satisfaction: How do users feel when they use the tool?
- Documentation: How clear is it for a user to get started, and can they effectively unblock themselves?
- Accessibility: Can everyone use the tool and understand its output?
- Scalability: Does this tool scale for the growth of the organization and codebase?
Two angles to this often make it challenging to focus on this principle:
- Focus on tomorrow’s user: It’s essential to focus not just on today’s users but to make systems scalable and maintainable for future users and the growth of a codebase, product, or organization. Even though we may focus on user value today, it may make more sense to prioritize the value the tool will bring to tomorrow’s user. De-prioritizing immediate work is in direct conflict with providing value to users quickly but will bring more value to everyone over a longer time.
- Do the boring work: Some developers will go to extreme lengths to justify going after shiny and fun projects. This list of priorities can serve as a counter-measure. An explicit user focus will almost always shut down grand ideas about complete technology rewrites in favor of the monotonous work. For example, instead of rewriting a dependency manager, what if you started managing your dependencies?
Here are two concrete examples of a user focus on “error signals”:
- The primary goal of a testing framework such as Jest is to ensure correctness. The secondary goal is to provide highly actionable signals immediately when something goes wrong. That’s why hundreds of hours of (free and paid open source) work went into designing Jest’s error output and intelligent scheduling to give actionable feedback to developers quickly.
- Rick Hanlon completely redesigned how to display errors in React Native. This change improved user actionable feedback and speeds up how quickly users can resolve programming mistakes.
Most tech stacks grow out of necessity, not through careful planning. And that’s a good thing! If all your business focuses on is your tools’ folder structure, you are unlikely to be successful.3 As organizations scale, more and more infrastructure tightly connects with other systems and becomes harder to change. It’ll usually take far longer to rewrite most existing systems from scratch than it would take to migrate them incrementally.
While it may be great to consider the ideal solution to all problems if we could start from scratch, the cost of change or the sunk cost is often so high that it’s simply not worth considering a full rewrite. Instead, I generally recommend creating incremental value. Done is better than perfect, and a few rough edges are acceptable if there is a viable path forward to a system that’s close-to-ideal.
I learned this principle through the incremental rewrite of Jest that started in 2015. Jest came with a few good ideas, but nobody was working on improvements. At the same time, Facebook heavily depended on it for various large projects. It wasn’t feasible to build an entirely new test framework and migrate tens of thousands of existing tests. Instead, I chose to separate concerns and rewrite individual pieces in-place.
Incremental Migration is the perfect principle to highlight what will happen when we violate it and why we might choose to do it. Let’s say we exhausted all other options and concluded our system wouldn’t keep scaling for the codebase and organization’s growth. We have to go for the daunting rewrite. We’ll start from scratch to do a long-term rewrite with a high chance of failure while pausing maintenance and improving the existing system. That’s painful! The point of priorities is not always to stick with them but rather to be aware of which trade-offs we are making when we do violate them.
But beware, I was involved in multiple projects that ultimately failed to achieve their goals:
- A rewrite of a package manager changed so many constraints it wasn’t applicable any longer.
To make sure we are forming the right opinions, we need to fully understand the system, bringing us to the next principle: Clarity.
I mentioned before that it is hard to justify work on developer experience. We have to focus on clarity, beginning with plans, narrative, and the definition of metrics or goals. We can apply clarity from the project management level down to the separation of concerns, APIs, and maybe even variable names.4 Focus on essential words to describe complex topics. Not only will it help with the narrative of plans or help people better understand the proposed work, it almost always ends up with less complicated infrastructure, too!
This experience taught me that it’s never too early to share a draft. Nowadays, I test high-level ideas or specific code changes I’m planning with people who have the most context. That could be people who worked on this infrastructure in the past, people I trust to give honest feedback, or a group of people willing to listen and bounce ideas back and forth. It frequently helps me understand downstream effects I wasn’t aware of previously.
Clarity ended up in my Principle Stack because I used to make the mistake that tools should get out of the way and be invisible. Now I am convinced this was misguided. I was masking complexity in the name of simplicity instead of surfacing the right level of complexity at the best interaction point with the user, which also directly led me to discover another principle:
Diving into an existing system often yields variants of harsh statements like “Wow, this is the worst design I’ve ever seen” or “Who would ever write such complex code for such a simple problem?” 5
Assumptions, constraints, and trade-offs tend to change over years or even in just a few months. It’s even worse to be aware of that and not do anything about it. At the same time, it’s essential to recognize that what was built in the past probably wasn’t bad, just that at the time, the constraints or assumptions were different. Gaining context on the past will help make better decisions and more extensive changes to existing systems in the future.
Be honest about trade-offs because most technical solutions will have negative trade-offs. Don’t pick solutions that will put you or your organization in a worse position in the future. Secondly, aim to make things easier instead of building complex abstractions on top of complex systems.
The final principle is about retaining or gaining option value, which means any change to a system should unlock more options for improvements and significant future changes. If we zoom out, these principles exist because we often got into a state with developer tooling that made it hard to introduce major yet gradual improvements. After all, there is usually little option value embedded in the design of existing systems. If we keep option value in mind when redesigning infrastructure, we can naturally adapt to new requirements in the future.
For example, monolithic toolchains cannot move fast as they often don’t have clear API boundaries. In such a case, we gain leverage over all parts of our infrastructure by modularizing it. Once we have a clear separation of concerns, we can have competing implementations of the same pieces that are well-defined in scope and can be swapped out with better versions as needed.
The list of principles could be endless. I generally find the sweet spot to be around five principles for any domain, each having a few sub-principles. If there are more, they tend to become hard to remember, and it is overwhelming to apply them to decisions consistently. There are many other contenders for DevX principles, though, and you may choose other priorities in your organization or projects. Some of them could be:
- Scale: Some organizations or tools may want to define an explicit principle on enabling various levels of scalability.
- Growth: In some places, especially early in startups that build developer tools, optimizing for user growth may be the primary principle that has a downstream influence on every decision.
- Complexity / Simplicity: While “Clarity” is good enough for me to touch on both complexity and simplicity, you may want a more well-defined principle on the complexity you’d like to avoid or the simplicity that is core to your product.
I shared high-level principles for how I think about the DevX space in general. Individual tools can apply these principles to identify their tool-specific principles. If you are the maintainer of a popular and well-scoped tool, you should absolutely define your principles, write them down and share them with your users. Ideally, your principles won’t change much over the years, and if they do, that might mean your project is changing substantially, too.
Here are three of my favorites:
- Figma’s engineering values: Principles for operating a team focused on design tools.
- React’s Design Principles: React’s principles were initially shared in 2016 and upheld through every significant change. They outlined half a decade’s worth of work and established how the team works.
- Fish Shell Principles: Fish shell is an absolutely delightful product. It’s easy to make a direct connection from using fish shell back to their principles.
If you squint, you’ll find that many of the principles also apply to other domains. In a follow-up post, we’ll explore how to measure DevX.
- See this excellent post about developer effectiveness in the meantime.↩
- Check out my all-time favorite podcast by Exponent on Principle Stacks.↩
- Unless, of course, your business is “Folder Structure as a Service”.↩
- Don’t be the senior developer who uses all the big words to paper over their imposter syndrome.↩
- Often followed by the sudden realization they were the original author of that piece of code, and then quickly taking back all the mean words.↩
- Check out this timeless article by Orta Therox on owning third-party dependencies.↩