Why the local development experience matters (a lot)

Unless you're completely invested in a cloud-based development environment, chances are you'll actually clone a repository and run its code locally. For me, how well that works is a crucial indicator of overall project health. But let's start at the beginning.

 
What's a good experience when working with a project? A project should be easy to setup, easy to run, easy to setup in a development environment, easy to debug, easy to profile and easy to test. It should be easy to work with it, to summarise. Ideally, all of those actions described should also be fast. Take sqlite as an example, a well known on-device storage solution. It is super well tested, runs on every dishwasher and has a rather long history - still, getting the source code and building it locally couldn't be easier: 

git clone https://github.com/sqlite/sqlite.git
cd sqlite
./configure
make

That's it. One command to clone the repository, one to setup the build and one to actually run it. And the whole show takes less than a minute. As a bonus, the README.md contains detailed instructions on how to run the build, customisation options and so on. Even though it's really simple, it's still documented. 

What's happening here is that the complexity in working with something is not put on the future contributors – it's tackled as part of the development process. Making it easy to contribute, making it easy to understand past decisions, making it easy to work with existing code is the exact opposite of technical debt. And while we speak a lot about technical debt, we probably don't spend enough time doing the little things that help so much against it accruing. 

And here it is: There's no excuse that your repository takes any more steps to get contributors up and running. Clone, Setup and Run. While cloning is the uninteresting part – if you're using git, it's safe to assume that most folks will be able to do a clone. Setting up a local repository to be in a state where you can run whatever it is you're working on is a little more tricky. Of course this depends on whether we're talking about a desktop application, a web app, a mobile application or a backend service. 

Shopify went in an interesting direction – and one that I learned to love quite a lot at my time there: a tool called dev. You can read a little more about it in this dedicated post, but the gist is that you'd have a dev.yml file in every repo, and you'd invoke dev up to configure a project locally, and dev run to run it. The key here was that it really didn't matter what kind of project you were dealing with. A mobile application would launch in a simulator, a backend service would launch an entry page and so on. The decision what makes most sense was already made for you – by the people who probably knew best. What really helped to reduce cognitive load was the convention: dev up and dev run. That's all you need. 

Coming back to the three steps, that really are only two that you should care about, let's decompose those a little more, starting with 

Setup

Setup is everything that needs to be done before building and running an app, except building. That is 

  • Installing and configuring the development environment 
  • Installing dependencies
  • Downloading necessary assets or files 
  • Creating, copying or initialising configurations 
  • Creating, copying or initialising data 

It's fine for this step to take a while – but the general idea is to have a script that runs and makes sure when its done you have either an error message or a configured local environment. It makes sense to be clear on what operating systems and versions you are actively targeting and supporting – if you're mostly working with Macs, spending a little more on getting everyone on the same stack might be cheaper than maintaining multiple development environments. 

Setup should be fast, but it's not critical – ideally, this should only be run rarely. So while it makes sense to get an idea of low hanging fruits to speed things up, I generally wouldn't go crazy here. The key thing, the one critical idea: it needs to be one command. Not two, not three: one. 

Run

Once your environment is setup, you ideally want to run the thing you're contributing to. This should again work with a single command, one that is then implicitly triggering a build and running the artefact (if possible). If the nature of the project doesn't allow running it – like in the case of a library that comes without a test rig, running the tests is a perfectly good alternative. Anything that leads to the code in the repository being executed works. 

Here speed is more crucial, since you'll run something more often than setting up the environment. Tap into whatever your build environment's super powers are to make that fast – incremental builds, build caches, you name it. Just keep in mind that that every second you're saving here will be beneficial to every engineer working with this thing. It's a worthy investment. 

At this step, you have two stages well defined – just like the folks from sqlite. Let's speak about tooling – and why it matters. 

Tooling

It doesn't matter what tooling you use, as long as it's consistent, available, reliable – and fast. Something that fits the bill nicely is make. It's installed on most machines with some basic build tooling, has support for multiple targets and is intended to be invoked on a command basis. An example Makefile, taken from a repository that contains a few d2 diagrams looks something like this 

all: setup run

setup:
ifeq (, $(shell which d2))
	brew install d2
endif

run: diagram.d2 
	d2 diagram.d2 diagram.html
	open diagram.html

With those 10 magic lines, you'll get a setup that will allow your contributors to clone this repo, run make and have the created diagram launched in a browser. If they don't have the tooling, it'll automatically install – no need to read documentation, get familiar with d2, it just works out of the box. That's the goal. 

A word on documentation

Even if you're following the same convention in all of your repositories, I'd always document the things you're assuming are installed – and how to install them, as well as the two commands for setting up the environment and running something. Put that on top of a README.md, and it'll be hard to overlook. Make sure it's part of the repository – this makes sure that even if you're checking out old versions, the instructions apply to the version you have in front of you, something that's harder to do using external wikis or documentation stores.

Why this matters

Having an environment that is easy to work with is critical from quite many angles. The one that's most easily understood is the economical one: If your team can't setup a repo without spending a few hours on calls and wading through documentation, you're just wasting time – a lot. It's also frustrating, so it's a no-win scenario with no considerable upside. 

There's a second level that I find quite telling when encountered. If you're asking folks to improve local developer experience and it's met with "that's really hard to do", the problem really isn't the local developer experience. Whether it's dependencies that are impossible to run locally, dependencies that require special massaging to work or the simple fact that it's overly complex to get everything prepared – all those are smells that should not be overlooked. Start your work right there. 

Lastly, not being able to get something up and running when you need to makes people feel stupid. I've spoken with more than a handful of engineers of varying levels of experience. A lot of them feel not like the smartest person when they can't get a repo up and running – even if it's really, really not their fault. 

I left a little checklist (inspired by the Joel-Test) to help you understand if you should be taking action in your team.

Local DX Checklist

  • Your repository has a README.md?
  • The README.md contains working instructions on how to setup the project?
  • The README.md contains working instructions on how to run the project?
  • The command for running the project is only one command?
  • The command for setting up the project is only one command? 
  • Getting the project up and running is completely self-sufficient – you don't have to ping someone to get a key file, seed data or something like that?
  • The command for setting up the project is working after a clean checkout?
  • The command for running the project is working after the setup step and actually runs the project?