If you start researching UX/web design, it won’t be long before you stumble onto Steve Krug’s “Don’t Make Me Think” — a book whose thesis is aptly summarized by its title. The best user experience is one where the user has to think as little as possible — getting what they need to get done is intuitive, each next step is obvious, and there is a minimum amount of decision-making and figuring out what to do next.
As a designer, we want to anticipate our users’ needs and potential mistakes, designing in such a way that it’s difficult (or even impossible) for a user to make a mistake. In his book “The Design of Everyday Things,” Dan Norman goes so far as to say that any user error is the fault of the design, not the user.
Reducing pain points is a desirable outcome for many reasons: we can’t be there to help our users if they make a mistake, and no one wants to have to wait for support to get back to them just to get a basic task done. The more frustration a user feels, the likelier they are to abandon a product.
Customers fund our paychecks, so we bend over backward to make things easy for them. On the other hand, a full-time employee is committed — it takes a lot more work to find a new job than to check out a competitor’s website, and solving problems is part of the job description. However, this shift in attitude leads to unnecessary internal pain points with expensive consequences.
Studies have shown that focus and self-control are like muscles — you get a limited amount per day before it’s exhausted. Cal Newport, in his book “Deep Work”, writes that most people have a maximum of 1.5 hours of “deep concentration” available per day (this is the kind of concentration needed for tasks with a heavy mental load — solving a problem you haven’t seen before, creative brainstorming, learning new complex knowledge, as opposed to simpler tasks, like checking emails or data entry).
Naturally, time is also a limited resource. Every time a developer spends 1–2 hours (or 5 minutes, or 10 seconds, over and over) solving a problem caused by a pain point that could have been anticipated and avoided is 1–2 hours they don’t have available to do their important work and leaves them with depleted concentration available for their remaining tasks for the day. If the same pain point is costing time from multiple developers across teams, this adds up quickly.
Rather than “don’t make my engineers think” — we want to avoid making our engineers think when they don’t have to, so they can save their time and deep thinking for the tasks that matter.
So what are some examples of applying (or failing to apply) this philosophy to an engineering organization?
Write clean code.
I take my code writing philosophy from the book Clean Code, which is to make my code as readable as possible by making it as close to written English prose as possible. That means verbosity — descriptive words in function names, generally avoiding abbreviations, avoiding nested ternaries, generally (though not always) preferring if/else statements to ternaries. Compare: if (isEntryValid) {submitForm()} else {displayError()} vs { this.quantity.isInteger() && this.name.length : submit() ? displayError()}. The first involves less thinking and deciphering to figure out what is going on.
Code to the lowest denominator.
Think about what will be most intuitive to a junior developer or a developer new to a repo. I had a former coworker who said they found nested ternaries easy to read, and they loved using them (you can probably guess — this was a senior engineer). Newer developers are already going to be the ones most likely to be struggling (and then they are going to ask senior engineers to help figure things out), and senior engineers are the ones who have the expertise to move through unfamiliar patterns more seamlessly — coding to prioritize the ease for newer developers will save the time across the team as a whole.
Write (and automate!) unit tests.
Having an automated unit test catch a bug costs the company a lot less than having a QA or a customer find them.
Use a linter to enforce coding standards.
It’s hard to remember every coding convention for a repo, especially in your first few months in a new repo, and even senior devs make mistakes sometimes. Having a linter like eslint or prettier (especially one that runs automatically) makes sure these errors are always caught.
Prioritize refactoring and cleanup tickets .
This is really a message for team decision-makers. A fast-moving company with new features and tests coming up regularly doesn’t usually have the foresight to architect every aspect of the repo from the beginning. As a codebase grows organically, especially if there are multiple teams working on it, things can end up inconsistent (at best) to broken (at worst).
This might be as simple as similar features being done inconsistently across the code base (creating pain points for developers but has no detriment to user experience) or as problematic as conflicting code (ie, two teams building two different versions of a component that both went live but cannot both be live at the same time, rather than having one component built with the tests both in that one component’s code).. Managers often seem to think a cleaned-up repo and new product initiatives as mutually exclusive, but in my experience, 1–2 points of tech debt per sprint can make a huge impact in maintaining a database without slowing down team productivity on core team goals.
Prioritize writing and maintaining supportive documentation .
While most of the companies I’ve worked at have had some amount of technical documentation, they have tended to be a “setting up the repo” doc and otherwise focus on nitty-gritty details of the more technical parts of the repo type of company. Only one of the companies I’ve worked at had a coding standards document. There is a trio of documents I consider required: repository coding standards, onboarding, and troubleshooting.
Coding standards documentation helps a new developer feel confident with the first few pull requests, knowing they aren’t missing small things that they’ll get a bunch of PR comments about.
Onboarding documentation goes beyond the “how to set up your repo” by including a list of the major technologies used in the repo (and perhaps links to recommended tutorials for those technologies), common team bookmarked URLs and browser extensions, as well as links to internal documents like coding standards, project glossary or troubleshooting. One company I worked at used a less well-known React State library, and I did not even hear of it until three months into working there. Once I did, I found a tutorial, learned the fundamentals, and found the code much more understandable. I would have loved to have had that info my first week.
A troubleshooting doc is where we gather screenshots and solutions to the common issues that pop up in your first few months in a repo before you are so familiar with the code base that you stop making them instinctively. Does forgetting to run npm install always trigger a certain indecipherable error? Great. Does realizing you’re not in the right environment lead to a completely blank browser with no errors? Cool. For new devs (and older devs who accidentally make an old mistake), it can save a lot of time to have those all in one place.
Choose “boring technology.”
Written by Dan McKinley in 2015, the article “choose boring technology” argues that established technologies have fewer “unknown unknowns” (bugs and failures that are yet to be discovered), as well as more documentation and support. On the other hand, newer, exciting technologies haven’t had the same opportunities to be used, vetted, documented, and de-bugged. Often, it supports your company and your developers to opt for the documented, known technology over the exciting, riskier technologies.
Automate linters and unit tests.
Using linters to enforce coding standards was mentioned in the last section, and the default setup is that developers have to manually run a command to run linters and unit tests. Of course, even the most detailed-oriented employees will forget things like this sometimes, and it’s not a good use of company time to have code reviewers point out unit tests are failing or there are linter errors (and then require an additional push, testing and code review).
Clear and detailed tickets for active tasks are one of the most important ways an organization can reduce unnecessary time expenditures. This includes making sure ALL requirements are included and that there are “acceptance criteria” that communicate what must be present for the ticket to be done.
Additionally, if a ticket is made from a slack conversation, include a link to the slack conversation. Usually, there is a lot of context in the slack comments that don’t get included in the ticket. For example, one team I worked on would mark tickets as “ready for work” after they were groomed even when they had outstanding questions for design or product that needed answers. This meant that when a developer was ready to start a task, they would then have to spend time chasing down a coworker (possibly waiting for hours if they had a heavy meeting schedule — or days if they were OOO) to get an answer before they could begin. They’d have to decide if they should pick up a different task while they wait to hear back (they don’t know if they’ll hear back in 2 minutes or 5 hours).
This would also lead to unnecessary sprint carryover because if there are one or two days left in a sprint, and you spend that time waiting to hear back on requirements, the ticket doesn’t get done on time. If the ticket was made out of a slack conversation, add a link in the ticket to the slack thread (I don’t know how many times I’ve seen a sparse ticket where so much context was left in slack and never added to the ticket itself).
Consider adding “dev notes” sections to tickets.
If your team includes junior developers or developers new to the code base, and there’s an engineer who already has an idea of how to get a task done, adding a section to the ticket on “dev notes” to your tickets can be very efficient. In my experience, it takes less than a minute to write down which components to look at or which strategy we’re using for this kind of work in the repo, and those pointers allow the newer devs to focus their attention on understanding and figuring out the components and code that mattered.
Find practical ways to manage announcements around required tasks and updates.
One company I worked at would announce updates to personal dev environments via slack. These announcements would get buried quickly by other messages. Many of these updates would be required — “we’ve just pushed x, after your next pull, you must run command y, or else the repo won’t run locally for you anymore.”
Of course, people generally wouldn’t pull until they finished whatever ticket they were working on, which might be a few days or a week later. Without fail, in the week or two following these announcements, you’d see developers asking for help because things won’t run, and it would usually take 1–2 hours of them tinkering and getting suggestions from others before someone would remember that announcement.
This is an excellent example of failing to anticipate very predictable pain points that cost the company hours of dev time across multiple teams. A potential solution would be some sort of integration with a ticketing/task management software that might automatically create tickets for all engineers to complete this task, or managers manually creating such tickets for their team members, so that the tasks are clearly seen (even for those who were OOO and come back to a mountain of messages), as well as tracked and kept top of mind with all other active tasks.
Hold retros.
Retros (short for ‘retrospectives’) are meetings where the team goes over what went well and what didn’t go well in a pre-defined time period (often every two weeks or once a month). They can be very effective in identifying pain points that aren’t big enough to initiate an entire meeting for but cause long-term issues.
Organize team calendars to support longer focus times.
In his book Deep Work, Cal Newport shares that there is a specific type of concentration — “deep concentration” — that is required for challenging problem-solving. Context-switching costs not just time but concentration — Chris Parnin, a researcher at Ninlabs, found that it takes developers 10–15min to recover their coding workflow after an interruption and that an interrupted code task takes twice as long and contains twice as many errors as an uninterrupted task. If you are scheduling team meetings, be mindful to schedule them in a way that allows developers large chunks of uninterrupted time to code.
Don’t reward “tactical tornados.”
Coined by John Ousterhout in his book “A Philosophy of Software Design,” the term “tactical tornado” refers to a developer who codes tactically — that is, they code for the quickest solutions — as opposed to strategically, where more thoughtfulness and planning go into the coding process. A tactical tornado pumps out code very quickly and may get through many more tickets than their teammates, but because the code is not thoughtfully designed, there is often extra work down the road as other engineers have to decipher and refactor the code. Unfortunately, managers often love them because of their high ticket completion rate (and unfortunately, managers also tend to undervalue the time engineers spend cleaning up code).
Look into tools that automate small process tasks.
One company I worked at set things up so that Jira ticket statuses would automatically be updated based on code changes — creating a branch automatically set the ticket to “in progress”, creating a pull request set it to “in review”, merging it set the ticket to “done.” There are slack integrations that will automatically set your slack to “away” (with notifications muted) when you are at a meeting in your calendar. It can be easy to dismiss putting time into cleaning up engineering org processes because customer-facing tasks feel the most urgent, and employees are paid to be here and to tackle and solve problems. In the words of a past manager, “they’re adults, they can figure it out.”
While it’s definitely a less pleasant work experience for a developer to have to jump through hoops or repeatedly tackle unnecessary obstacles, at the end of the day, it is the company that pays the price. When developers spend a few hours a month fixing issues that could have been automated, those are hours that could have been spent building an exciting new customer-facing feature. When a team has to abandon its roadmap for six weeks to get their last 6 months’ worth of work function in the second set of components another team built, that’s 6 weeks of an entire team’s development production the company won’t get back. When 2 years from now there’s a bug in legacy code with no documentation, the 3 weeks instead of 1 week it takes to diagnose and fix the bug is time the company never gets back. When new developers take 6 months instead of 2 to feel comfortable in a new repository, that’s 4 months of reduced output than what the company could have achieved. We already understand anticipating and reducing pain points is how we support our customers (“they’re adults, they can figure it out” is a mindset that will lead to a lot of lost customers and revenue), if we adapt that mindset to engineering cultures, we support our developers now, as well as our code base’s long-term resiliency.
Don’t Make Me Think by Steve Krug
The Design of Every Day Things by Dan Norman
Choose Boring Technology by Dan McKinley
The High Price of Context Switching for Developers and Ways to Avoid It by Nitin Pande
The Limits of Self-Control: Roy Baumeister on the Effects of Willpower Depletion
Lead image Source.