Local First Development
If pressed to define how I approach programming, the words “pragmatic essentialism” will be used at some point. Do what works and make it as simple as possible to get the job done across time. This manifests itself in various ways in my day-to-day work. One of the rules I try to hold to is that the set up of a project for development should be as localized as possible. Put another way, the ability to test programming work should not depend on network access. Sure, you will need access to your SCM server to get the source code. And the internet generally to read docs, search Google, rant on Twitter about your issue, and find ideas/answers on Stack Overflow. But that’s you. The system that you’re working on should work on your machine in a way that mirrors the target environment as much as possible.
Note, I am not saying that “it works on my machine” is sufficient. I am saying that if it is feasible that an environment can be set up on my laptop, then that’s how it’s going to be done. This minimizes external dependencies and enables productivity. If, for example, I’m working on an application that uses a Postgres database for persistence then a local instance is preferable to a remote install. Especially if said install is only accessible on the corporate network (as should be the case). So long as I’m in the office, the latter will work, but if I’m traveling or working remotely then it’s just unnecessary friction. It’s also unnecessary friction to manage access to that remote database across a team or teams. Now you have to manage different users on that database so that your programmers aren’t interfering with each other. You also have to manage one-off settings for all the developers on their machines. Much easier to just configure
localhost:5432/db for the local configuration and have everyone move on to solving the real problems.
Tools like Docker and Vagrant have made this much simpler. If you aren’t familiar with them, you should be. And the sooner the better. With them, you can run databases, other applications, messaging systems, most of the things that your application depends on. Having said that, there are some things to keep in mind when doing local-first development.
Local datasets are often limited. To keep things light and fast, it’s often better in terms of developer productivity to work with a local dataset that is a representative sample of actual data. As with all tradeoffs, there is a downside. Using a small dataset can lead to different behavior patterns between local and deployed instances. If your local database is setup with a 2k SQL script, that’s obviously going to be different from your 500GB production database. Which touches on one of the other rules for programming - testing is essential. It’s acceptable to prioritize speed in your local development, but only if you have a rigorous testing process that will validate your code against actual conditions. You need at least one preproduction environment with real-world data where you can performance test your changes.
Make it easy to override the local development setup. Sometimes you need to access real services, data sources or environments. Set up your project to facilitate such overrides. This can be with environment variables, command line flags or some other mechanism but make it easy to do without hacking around in multiple configuration files. You especially want to avoid mechanisms that would screw things up when accidentally committed to version control.
Sometimes the work is complicated and difficult. Perhaps more difficult than it needs to be, but that’s the way of the world. If your application depends on three databases and seven services (each with their own datastore and dependent services) then this will be a challenge to fully replicate. In similar fashion, you are unlikely to mimic all the infrastructure provided by AWS on your laptop. In these cases, good application design practices can help provide a solution. If your service interactions are abstracted with adapter and facade type patterns then you can provide runtime implementations that act as mocks or doubles. You can reuse that same code in your unit and integration testing. Speaking of testing …
Another important consideration in all this is feedback. Local databases are useful and so are all those tests you should have for your application. As with water, food, scotch and other non-negotiables, it is possible to have too much of a good thing and lose sight of the primary goal, productivity. If the feedback cycle for testing is longer than a couple minutes then you need to partition your tests into categories that focus on speed and value. A test suite that takes ten to fifteen minutes to complete is a suite that will not be executed often enough. So run the fast, critical tests frequently and save the full suite for a pre-commit step or your continuous delivery pipeline.
You have choices and tradeoffs to make. If you have a challenging (and likely absurd) deadline twelve weeks out, you should certainly not spend six of those weeks creating a set of doubles to make things work locally. You should, however, still use good design principles and development practices like unit tests. Running locally is not the goal. It’s a means to and end, that end being to deliver value and solve problems.
Remember when doing development work, part of the path is opt local.