Developing Solo
How to write a production-grade project as a one-man team (or a small team)
Developing a software project solo is no easy task. With no one to unstuck you, review your code, or provide guidance, you are completely on your own, venturing into the unknown.
Mostly, inexperienced developers will either:
- Take this as an opportunity to disregard code quality standards and pay no attention whatsoever to code convention.
- Do the complete opposite and over-engineer everything way beyond what is needed.
However, as I will try to demonstrate in this article, both approaches are equally damaging. So without further ado, let’s go through a few points.
Note: If you are looking for practical advice, skip ahead to the Specific Recommendations section.
Know your enemies
There are two enemies you need to avoid and I do base lots of my opinions on that. In fact, you can consider this a TL;DR of this article.
- Willpower: Make better easier
This is your enemy as a human being, generally speaking. We place a lot of trust in our willpower, a very unstable and limited resource. You will write good code when it’s easier than writing bad code just like you’d go to the gym more often if there’s one in the same building. - Verbosity: Type less, achieve more
An unbelievable amount of time can be wasted copying boilerplate code around and forgetting to add that little property here or change that string there only to find out one hour later that it’s causing the weird bug you have been struggling with. As a solo developer, this problem can be frustrating at times as so much verbosity definitely impacts your valuable limited productivity.
You must feel “comfortable”
Making sure you are comfortable with the environment is essential. Having a personal template that you use to kickstart your projects helps a ton, however, this might not be applicable for new domains of development you are tackling. So, you need to master the art of getting yourself comfortable.
The first thing you usually choose, if you don’t have a favorite already, is the IDE. It’s essential to have proper IDE support. Here are the most useful IDE features that are quite important for all developers and an absolute must when developing solo:
- IntelliSense and spell-checking.
- Error detection and quick-fix suggestions
- Automatic code linting
- Auto-generating and refactoring code snippets
- Step-by-step debugger (that can even step into libraries)
- Quick shortcuts to easily navigate and modify your code (quick find, renaming, documenting, etc)
Surely you can pull off a production-ready codebase in vim! Albeit within the next 10 years or so.
Automation, automation, automation
If it can be automated within the time it takes to do it over the course of 3 months, automate it.
Let me clarify that last sentence a bit more with an example. If you have to test a specific critical use case every time you deploy and it takes 5 minutes to do so and you deploy 10 times per month, then that costs 50 minutes per month (assuming you don’t skip it or forget it every now and then). Now this means that the cost over 3 months is 150 minutes and thus, it’s completely acceptable to spend up to 150 minutes of your time writing an automated E2E test for this particular flow even if you have other pressing matters placed on-hold.
Out of all professionals out there, you as a developer should embrace automation to the max!
One more good reason to write automated tests is that with no one reviewing your code, there will be more room for error. And since no one in their right mind wants to manually test every feature of their application every time they release a new version, you need a lot of automated testing. The rule of the thumb is “if it breaks the flow of user experience, then write a test for it”. Don’t go as far as to write tests for aesthetic features unless you really have the time for it. It’s particularly useful though to have snapshots of your UI as part of your unit testing to watch for UI parts going MIA (e.g. StoryShots).
You must also have an automated build, release, and deployment pipeline setup. You simply don’t have the resources to do all of these things yourself every single release. This can be a template that you reuse for different projects as this type of setup takes quite a long time to get right but is usually very generic and reusable.
In addition to proper testing, you should have some sort of staging environment as identical to the production environment as possible. Releasing into production directly with no beta testers standing by and with only a handful of high-value users can be disastrous. If you set up your CI/CD pipeline properly, it’s very easy to deploy to staging on feature branches and catch those production-only bugs before they slip (TLS cert issues, misconfiguration, CORS errors, etc).
Pay attention to security
You will be committing the cardinal sin of debugging using production APIs/Databases quite often. Even if you say NEVER right now, you will, it’s tempting and often practical. So, without any team-shaming involved it’ll happen, eventually. Stepping through that stubborn bug that you can’t reproduce in dev line by line is just so satisfying, and alluring.
So make sure you have a way of storing passwords locally instead of committing them to the repo. No one is there to see them, but purging repositories later and getting commit hashes and version tags messed up isn’t fun. Additionally, what if your git provider is hacked one day? What if you obliviously give access to someone who isn’t supposed to see them? Keep passwords on your machine in an environment or secrets file or even in your local IDE variables.
Over-engineer, with diligence
Considering that over-engineering is always going to be a threat to a solo developer with no one judging and lots of precious time to potentially save (the root of any over-engineering prospect), let’s explore how we can mitigate the effects. As a solo developer, there are two types of over-engineering.
The Bad: Anything that adds verbosity.
The Good: Anything that reduces verbosity.
The further you over-engineer something, the more modular it becomes and the less logic you have within similar modules. In a good over-engineering example, the resulting modular code should be so small that you can type it from memory when creating a new module. You should also make sure that a simple IDE lookup can often find the exact line of code you want to change or at least one that leads to it (few duplicates and no vague flows the IDE can’t follow).
A very good example that I can recall now off the top of my head is Redux Saga vs Vuex Proxy. Both are very generalized libraries. But if we compare using sagas with all these verbose definitions and multiple layers vs using a proxy that generates mutations on demand when accessing the state… I guess we all know what’s better for a solo developer or even a rather small team.
Keep things organized
Whether it’s the code itself or different services your project is comprised of, keep things modular and standardized. This keeps your mind clear and your eyes quickly get used to the code. You’ll be surprised at the things you can now do on auto-pilot. So good code style and the use of containerization (e.g. Docker) are your best friends.
The Donts
Let’s go a bit more in-depth with what you should avoid as a solo developer or a small team:
- Stay away from high performance but bulky languages and/or frameworks. You want your code to be minimal in size as you can’t afford to type/copy tens of lines of boilerplate code to achieve trivial functionality. You also need to be able to easily debug and step into any line of code. If enough users are using this service to warrant a performance upgrade of this type, chances are there will be a rather big team working on it.
- Do not use languages that are not strongly- typed. You need to be able to see errors in development as often as possible and the advanced IntelliSense is a big plus you don’t want to miss on. The only exception to this is when you are working in a very specific domain that benefits from a specific language like ML and Python. But even then, you could still benefit from static type checking.
- Do not go overboard over-engineering. One of the best ways I found to achieve that is to get all the over-engineering out of your system while setting up the project and building the most basic non-business requirements of it.
- Don’t trust your memory. Whenever you use a bash line or a tiny script to do something, save it and document it. You can make use of your GitHub wiki for instance or just use the README file. It’s also nice to have a “scripts” directory on your project and to make use of the npm/yarn scripts on node projects.
- Don’t use willpower too much, make better easier
These, for example, are my trusty scripts on node projects which serve as a demonstration of point 4 and 5:
"git:commit": "git status && git add -N . && git add -p && sgc",
"git:stash": "git stash --all",
"git:nah": "git checkout . && git clean -df",
"git:amend": "git commit --amend --no-edit",
Specific Recommendations
Data Pipelines / Machine Learning
Dockerize your projects and don’t rely too much on your personal machine being set up correctly. Even if you need to make use of your personal machine (e.g. local GPU), you can achieve that with a local Kubernetes cluster.
Backend
So as mentioned above you need to stick to strongly-typed languages and avoid verbosity. That means Python or Go is probably not your best option. Something like C# with Entity Framework personally appeals to me because of the ridiculously good IDE support whether it’s Rider or VS and the crisp clean ultra-readable architecture of the framework. If you can do most of what you want with intuition and a bit of IntelliSense without having to google it or read the docs, that’s the biggest win for a language/framework combination. Java is ok if you are a masochist (not a fan of java myself).
Frontend (Web Apps)
I like both Vue and React when working solo as long as the project is TypeScript based. However, if it’s a big UI with lots of dynamic API integration and you are solo then definitely go for Vue. If you are using class components and Vuex module components (with the vuex-class-component package) fetching and saving data using your API becomes super easy. Having 2-way binding in any component becomes as easy as defining a class property! No need to worry about mutations, getters, setters, or any boilerplate code. That and hydrate/save/clear actions on the store module (with a hash and expiry timestamp for caching if you are classy) and you are good to go!
Architecture
It might seem counter-intuitive at first but micro-services, although the hallmark of develeopment in multiple teams, are your friend.
- You are the sole owner of all the services and have absolute control over what they do and how they do it, that already avoids alot of the trouble that comes with microservices.
- You can use the same hosted instance of generic services with multiple apps (e.g. mail or push notifications). Whether this is a personal project or a contract you are doing, this is useful.
- If you make a mistake somewhere and it slips past all measures and gets deployed (there’s no operations team or QA to catch it after all), it doesn’t affect the whole application; only that service will go down or have degraded performance.
- You can start new modules of your app those serve a different purpose without threatening the stability of the existing services (what if you want a chat app to offer voice/video calls?).
In addition to that, make sure all your services are Docker-ready. Not only does this mean you can easily deploy them anywhere and have a nice production environment replica in staging and even development, but you can also leverage Kubernetes or AWS ECS and scale your app with a day’s work whenever needed!
Conclusion
In the end, remember that this article is entirely based on the premise that you are working alone or with a small team and within a limited time window. Most of the software conventions we have today are built around development in big enterprises with huge teams so obviously, don’t expect solo rules to be a 100% match to the conventional wisdom.
And again, the TL;DR of this article is “avoid verbosity and make better code easier without draining your willpower”.