Skip to main content

Finding Efficiency with Clean Code and a Strategic Mindset

04-05-23 Emma Litwa-Vulcu

From a technical perspective, efficiency in development—an MVP gold standard—is about a lot more than speed. In fact, sometimes you have to move at a measured (perhaps uncomfortably slow) pace to save time overall.

Efficiency takes more than just knowing your keyboard shortcuts, having experience in a framework, and being good at git. Efficiency is about spending less time doing menial tasks; it’s about looking at—and understanding—the overall picture. Most importantly, it’s about writing good code. Bad code—code written quickly, without thought, without refactoring; code that cuts corners—leads to massive tech debt. It introduces sluggishness in all future development that touches it and will make pivots take months instead of weeks. The longer you build on bad code, the more your team’s agility will plummet.

I see it all too often. Sometimes it’s due to a lack of experience (or care). However, I also see bad code written by very skilled developers who care a great deal about their work—particularly when teams are working on an MVP (Minimum Viable Product) or are otherwise being pressured to deliver quickly. An MVP is a stepping-off point, and developers should be setting their clients up for success; writing code that is unmaintainable (even when it saves you time in the short term) is not doing that.

Seeking quality and clarity will, conversely, keep you and your team efficient and agile. Good code—clean code—sometimes take a little longer to write. When building a new feature, or working on an MVP, that often means going back and refactoring. Sometimes, particularly early on, the entire architecture may need to shift. That’s okay. Taking the extra time to write clean code will massively increase the efficiency and agility of yourself, your team, and the company or client you work for. Spending an extra day on a new feature, or an extra week or two on an MVP, may mean a difference of months of work or more in the long run.

Take the Time to See the Bigger Picture

Project Management and Tech Leads (and Developers)

Both project managers and tech leads should be looking at the project from an objective and key results point of view. That is, aligning top-down strategy with bottom-up team commitments and sprint planning. They should also be working directly with the client to determine, and strategize development around, short- and long-term goals. Creating alignment between dev team sprints and related tasks and client objectives means happy clients.

As a way to efficiently align goals and tasks—rather than asking the client what they want you to do, or what they want you to build—ask them what they want to accomplish. It’s likely that you’ll pretty quickly gain far better insight into what development tasks could best achieve their goals. The better handle you have on client objectives, the quicker you’ll be able to achieve them without wasted or misdirected effort.

While most developers on a project are going to be looking at it from a more technical perspective, this mindset is still relevant and should be your attitude from the early stages of a project.

Development

From the perspective of a developer, take the time to smell the roses. Seriously! If you’re working with an unfamiliar API, library, framework, or just a series of components, take the time to learn how the code that’s already written fits together into the whole. Understanding what you’re working with on a more fundamental level will allow you to better grasp how the project in its entirety works. Taking the time to learn and understand at the beginning will massively speed up your ability to write new code and debug existing code for the rest of the project or, if you’re learning a framework, for as long as you work with that framework.

And when you need to debug things, having perspective on the entire code base will allow you to quickly assess the root cause of a bug instead of trying to hack together a fix within the immediate scope of your work. When you’re addressing the effects of a problem and not its root cause, you’re making the code base convoluted and harder to maintain. Even when you’re not fixing a bug, make sure you’re using the tools available that are made for the problem you’re trying to solve and not just the first thing you found that works.

Write Clean Code

I’m going to harp on this again. There’s a lot of bad code out there. We’ve all seen it. And sometimes we’re guilty of it. I love to follow the adage, “don’t get it right, get it written.” Or, in programmer-speak, “avoid early optimization.” Yet I’ve seen too many developers take this too far and into the realm of “we’re moving too fast to refactor or write documentation.” Or even worse, “move fast and break things.”

If you’re not writing clean code as you go, you’ll get to the end of a feature or project and it will never be cleaned up. We all know how backlogs work—we get through the things the client needs first. The client always needs something. If they don’t, they probably aren’t currently paying you. Clean code? That’s a nice-to-have. If you’re putting refactoring on the backlog and not prioritizing it as you go, you’ll never get to it. And that’s doing the client a disservice because it costs them more money in the long run. Don’t let little “oh it’s close enough for now” items slip through the cracks. Don’t just get it written. Once it’s written, get it right.

Leave Things Better Than You Found Them

This phrase is oft-repeated at Sparkbox and is massive in terms of preventing or even cleaning up tech debt. It’s easy to take a task and complete that specific task only, never mind the code around it. However, a little bit of refactoring around your code or some added documentation can go a long way toward a more cohesive and coherent code base. And even if that’s not the case, if you see something that can be cleaned up (that falls within your current scope of work), clean it up!

With that in mind, it’s important to note that this requires other developers to not be territorial about their code. People are often proud of what they create (and they should be), but sometimes that pride can bleed over into being protective of their code and being wary of anyone changing it. The thing is, though, the code base isn’t yours. It’s a collaborative effort from everyone involved, and it belongs to the client or the company you work for. Good code also requires collaboration and humility.

To reframe this in the context of tech debt, you can break up what might be an overwhelmingly large list of tasks by making the work of cleaning it up an everyday practice.

More Code is Not More Better

“Keep it simple, stupid” is a core tenet of writing code. Be succinct. Don’t write code you don’t need. More code is more tech debt. Don’t repeat yourself; avoid repetitions of patterns and duplication in favor of abstractions. Don’t overcomplicate matters; always consider cognitive complexity. Don’t abstract for the sake of abstraction, but abstract when it makes sense—too much abstraction and it’s too difficult to understand the underlying code. And don’t write more functions just because you can.

An oft-touted rule of thumb in writing clean code is, “every function should only do one thing.” But you don’t need a function to add two numbers together. You definitely don’t need a function that does nothing but call another function. (There are exceptions to this, but they should be really compelling exceptions.) Consider this rule from the perspective of the end-user of your code: Regardless of its contents, each function should have a single purpose. The contents of the function shouldn’t be relevant. If a function has to do three different things in order to achieve its single stated purpose, that’s okay. You don’t need to write three different functions for its internal code. Unless, of course, that code is reused in other places or it would significantly reduce cognitive complexity.

Develop for Developers

Learning to understand a project from a client’s perspective (user-centric thinking) is a skill we all strive to be great at. But when we think about a client’s perspective, we typically think about things like user experience and ROI. Learning how to contextualize those things will make you a better developer, but it won’t necessarily make your code any better. As important as these things are, they aren’t skills that will improve your efficiency.

However, in the same way we strive to understand clients’ perspectives, a good developer should strive to understand the perspectives of other developers. Don’t just ask questions like these:

  • “How will the client use this feature?”

  • “How is this group of buttons better than a different group of buttons?”

Instead, ask questions like these:

  • “How will other developers use this class?”

  • “How is this organization of methods and parameters better than a different paradigm?”

  • “Is this documentation clear to someone not familiar with the project?”

From the perspective of writing a library, which of the following code examples is more efficient to use and easy to understand? Each generates the same gradient of colors.

import * as Color from 'chromaticity-color-utilities'

const intialColor: Color.HexColor = new Color.HexColor(0x9a237f);
const rgbColor: Color.RgbColor = Color.desaturateRgbColor(Color.hex2rgb(hexColor));
const hslColor: Color.HslColor = new Color.HslColor(300, 60, 45);
const schemeRgb: Color.RgbColor[] = Color.createRgbGradient(rgbColor, Color.hsl2rgb(hslColor), 5);
let schemeLab: Color.LabColor[] = [];
schemeRgb.forEach(color => {
  schemeLab[] = Color.rgb2lab(color, 'AdobeRGB', 'D50');
});

vs

import Color from 'chromaticity-color-utilities'

const scheme: Color.lab[] = Color.from('hex', 0x9a237f)
  .modify('desaturate', { amount: 0.2 })
  .to('lab', {
    colorSpace: 'AdobeRGB',
    referenceWhite: 'D50',
  })
  .scheme('gradient', {
    with: Color.from('hsl', [300, 50, 45]),
    colors: 5,
  });

By utilizing method chaining, inheritance, and named parameters, there’s far less cognitive complexity in the second example. This speeds things up for the developer using the code, as well as for any developers who later read the code. It should be noted that this isn’t a critique of the above code—the issues with the above code are with the library, not the code written in this example.

When you write code that’s easy to use, it often organizes itself. You’ll speed up development not just for yourself, but for every other developer that uses your code.

Writing Good Documentation

Speaking of writing code for developers, take the time to write (good) documentation. I can hear you, right now, yelling, “but that takes more time!” Yes. Yes, it does. Guess what, though? It saves far more time in the long run than it costs, particularly for every developer who looks at or uses your code after you. Also, write as you go!

This doesn’t mean you need comments in every commit. You might find yourself spending a day or two writing and rewriting code just to get something to work, but once it does (and once you’ve refactored, if need be) write your documentation! If you think you’ll come back to it later, you won’t. If you think you’re moving too fast to write documentation, you aren’t. You’re slowing yourself down by creating problems to slog through later; for yourself and everyone else on your team.

If you’re building a new feature, your feature isn’t worth much if no one knows how to use it. When working on an MVP, you’re setting your client up to build on it. The “V” in MVP isn’t just for end-users. An MVP is supposed to be an initial stepping stone. If you aren’t writing good documentation, you aren’t creating a very viable stepping stone.

Writing good in-code documentation consists of two parts: the declaration and the definition. The declaration documentation should specify how to use your method/class/function as a black box. How the function works internally shouldn’t be relevant here. The definition, alternatively, should explain not what the code is doing, but why.

Anyone familiar with the language should be able to look at your code and grasp what it’s doing. If they can’t, or if you feel like a section is unusually complex or obscure, you should probably look at refactoring for clarity rather than adding comments explaining what your convoluted code is doing (there are, of course, exceptions to this rule).

What isn’t written in the code itself is your thought process; why you chose to write one thing over another, why something is necessary, and (on occasion) why something might be overly complicated and not immediately clear to every developer that comes across it.

tl;dr: Write how for the declaration and write why in the definition. If you’re thinking what, you may need to refactor to reduce cognitive complexity.

Group Similar Tasks

Typical practice when looking at the to-do list for a project is to grab the next item on the stack. This generally works great for a variety of reasons. However, when there is an extended list of issues that all have the same or similar tasks, or work on the same class or component, or address related bugs, it’s often more efficient to split a team—or let the team split themselves—into categories of work. If you have a larger team, configuring swimlanes can streamline this.

Spend your “Money” Where Your Time Is

You spend a lot of time in your office chair. Buy yourself a nice one—your back will thank you. Similarly, think about where you spend a lot of time in development, repeating the same tasks. As an example, I’ve spent a not-insignificant amount of time optimizing my git workflow.

At Sparkbox, we use a rebase workflow in Git. This means I spend a lot of my time in Git rebasing and squashing. Typically, a merge workflow for us might look like the following:

git switch main
git pull
git switch ABC-123-jira-made-a-branch-name-just-for-me
git rebase main
# Count commits in branch, or copy a commit revision
git rebase -i HEAD~10
# Change `pick` to `drop` for any temporary commits, then
# swap `pick` with `squash` on every commit after the first (SO MUCH TIME!)
# Rewrite commit message
git push --force-with-lease
# Wait for CI
git switch main
git merge --ff-only ABC-123-jira-made-a-branch-name-just-for-me
git push

By writing a handful of functions and aliases, the same workflow can become much shorter:

gswf 123  # switch branch
grom      # rebase on main
gbsquash  # squash branch
# Rewrite commit message
gfp       # push force-with-lease
# Wait for CI
gmffthis  # fast-forward merge the current branch with its parent
gp        # push

Where:

CommandDescription
gswf [search_string]Search local branches first, then remote, for a jira issue number and switch to that branch
gromgit pull [remote] [main_branch_name] then git rebase [main_branch_name]
gbsquashUses interactive rebasing to squash a branch, using a sed command as the editor that swaps pick with squash and drops any commits beginning with drop: automagically
gfpgit push --force-with-lease
gmffthisgit checkout [parent_branch_name], git pull, then git merge [branch_name]
gpgit push

These could be shortened further fairly easily, but I like to keep these commands somewhat separated in case of encountering unexpected issues. If you don’t like acronyms, use whatever you like. grom could just as easily be rebasemain.

Additionally, for all the love I have for the command line and encourage developers to rely on it, a GUI makes way more sense for tasks like cherry-picking. Don’t be afraid to rely on the tools that speed you up.

A screenshot of a Git GUI, where the user is cherry-picking a commit.

Make the Squigglies Go Away

IDEs (Integrated Development Environments) are your friend. They aren’t there to code for you, but they do tell you when you’re missing something.

Not all IDEs are created equal, though, and one IDE is likely not the best for every project you work on. Additionally, plugins/extensions may be available for the framework you’re using. With no steps taken at all, most IDEs can point out basic syntax issues with your code. With a little bit of work, your IDE can lint for you. With a little more setup, your IDE can give you intellisense squiggles too. Particularly for bigger, enterprise projects intellisense will save you significant time in hunting down bugs.

The following is the same code, from the same project, with the same linting rules, opened in different IDEs.

A screenshot of code in an IDE with no squiggles (IDE hints).
A screenshot of code in an IDE with lots of squiggles (IDE hints).

Issues include:

  • No description for a parameter

  • No description for a return value

  • Doc block does not include @throws

  • No parameter types specified

  • No return type specified

  • Return type mismatch (x3)

  • Potentially polymorphic calls (x3)

  • Method not found

  • Spellcheck (x2)

None of these were picked up by the first IDE. Running the code would have picked up the mistyped method name, but the rest might have gone unnoticed. The more squiggles you can make your IDE show, the quicker you’ll see and be able to resolve issues before you even make a commit. This directly translates to lower tech debt, better maintainability, and more agility.

Conclusion

The oft-shrieked, “move fast and break things” is one of the worst adages. (Meta, who coined the phrase, has tried to forego this misguided ideal, but has been slow to get there.) If you’re moving fast and breaking things, you’re setting yourself up for an awful slog down the line. This adage boils down to another adage: “Save a penny, lose a dollar.”

Code, as most developers know, is both an asset and a liability. But the liability part of the equation is not just the potential for bugs—it’s the amount of work spent on maintenance and developing new features. Writing good code optimizes your efficiency overall and shifts the balance of a project to be far less of a liability and far more of an asset. Getting to an MVP is about more than just writing something that technically works because if you’re not writing it well, you’re wasting your clients’ money.

Related Content

User-Centered Thinking: 7 Things to Consider and a Free Guide

Want the benefits of UX but not sure where to start? Grab our guide to evaluate your needs, earn buy-in, and get hiring tips.

More Details

See Everything In

Want to talk about how we can work together?

Katie can help

A portrait of Vice President of Business Development, Katie Jennings.

Katie Jennings

Vice President of Business Development