We had “Software is never finished, only abandoned.” While this is true, it doesn’t tell us anything helpful. So let’s rephrase this into something we can act on: “Software is never finished, only released.“
Let’s talk about releasing your product, and what makes this process special for bootstrapped founders.
In most cases, technical founders will be able to do Cowboy Coding, which means that they have full control over the development and deployment process. It also means that reliable systems are not sufficiently established, as things are “done the way they are done,” particularly in the beginning phases of a startup.
If you want to set up your release process in a future-proof way from the beginning, here is what is most important about releasing your product in a bootstrapped business: release often, release early, and release safely.
That way, your engineering and release culture will allow you to be nimble, react to customer feedback quickly, and build a stable infrastructure for the essential asset of your business: your product.
When I worked for a VC-backed startup at the beginning of my engineering career, we would release expansive feature sets, sometimes months apart at a time. As a result, I would work on many different small features. There was a lot of extra work whenever one developer’s code conflicted with another developer’s, and the days following release day were full of customers reaching out about problems — all at once.
You can prevent this by releasing more often. At FeedbackPanda, I released almost daily, and at least once a week. Whenever a feature was done enough, it would be released. I didn’t wait for a release day; I just made sure I wouldn’t release while most of our customers were using the product, which was a 3-hour window I would usually avoid for anything that was not a bug fix.
Releasing often prevents horizontal overengineering: you won’t work on too many features at the same time. In the best case, every release is precisely one feature, with all of your focus going into getting it right the first time. By releasing at least once a week, you will force yourself to concentrate on a minimal number of things. This focus on single features allows you to build many small things over time, one after the other, and immediately validate if they are useful or not. If you bundle them up into one big release every few months, your validation data will lose a lot of accuracy and granularity.
Releasing often allows for feature implementation feedback: you can learn very quickly how well the released feature solves the problem it’s supposed to solve. Since you’ve delivered a single feature, you will be able to attribute any unexpected side-effects to your release. Many client-side error monitoring tools can assign a suspect release to every new error, making an educated guess as to which deployment might be responsible for a newly encountered error.
Releasing often allows your product to evolve progressively: no change will ever make a customer think that “everything has changed.” Customers get used to your release cadence, and they will have a much easier time integrating small changes into their workflows.
Don’t forget to keep your documentation and Standard Operating Procedures up-to-date. For engineers, it’s a lot of fun to build, and a great joy to see customers use newly released features. The part where you take new screenshots for your knowledge base and write new step-by-step guides for your Operations Manual may not be as exciting. But those steps, when taken routinely, will make sure your business runs on as much automation and documentation as possible, which will make it more sellable in the future.
When I released a feature of FeedbackPanda that would allow our teachers to share their feedback templates with each other, I released a crude version that took me two days to build. It worked only for one of the schools we supported, it had a very basic interface, and all our customers could do was find a shared template and import it into their database. This feature eventually turned into what we called the FeedbackPandaCloud.
After we released the feature, our most adventurous customers started using it, and they reached out with both positive and negative feedback. They explained at which points in their workflow they found this feature to be an improvement and where it confused them. A day later, I had addressed many of those initial concerns.
The next iteration of the feature happened a good week later. In the meantime, I deployed several bug fixes and small improvements to other, more integral parts of the product, some of which were related to the interface components used in the sharing feature.
Releasing early prevents vertical overengineering: you won’t work on one feature for too long. After you release a feature, you’re free to work on something else. Not focussing on one feature for too long prevents going down rabbit holes, which is a big concern for many solo founders.
It will also make your features very succinct. By releasing when features are just a few days old, you will effectively be releasing MVP versions of individual features. You won’t have months to fiddle with irrelevant details if your feature needs to be good enough in three days.
Releasing early allows for feature integration feedback: your customers can tell you very quickly how well the feature fits into their existing workflow. This is great because every feature you release will need to be tweaked in some way eventually. Since you want to provide the maximum amount of value to all of your customers as fast as possible, getting valuable feedback early in the lifetime of a feature is a significant advantage.
If you work for months on a feature only to find out that you misunderstood the nature of your customer’s problem, that is a month wasted. If you can find that out after working on the feature for just four days, you saved yourself three weeks. That’s why releasing early is important: it frees up time for other, equally or more critical opportunities of creating value for your customers.
Releasing early allows for features to evolve progressively: you will make small changes to your features over time, responding to real-world usage and customer requirements. No battle plan survives contact with the enemy, and as much of an expert you might be as a founder, the expert of what your customers need will always be your customers themselves.
Iterating your features is a good practice because you will need to make changes anyway, either from customer feedback you receive or when you learn something new about the workflow of your customers. The chance that you can release a feature once and never touch it is slim, and the changing nature of both your business and the needs of your customers will make you revisit your product often.
If a feature is lacking a critical component, you will learn about this very quickly. In the case of our FeedbackPandaCloud, people requested a way to edit templates before importing them into their own database. Since I had started building the feature with a skeleton functionality, I could easily add new features whenever I needed to.
It’s not just customer requirements that make this progressive approach attractive. In some cases, your features don’t need to be fully-fledged because it doesn’t yet make sense for your business. When I integrated a subscription payment system into FeedbackPanda, all I needed for the first couple of weeks was a way for people to subscribe. The first public version of our service had a subscribe button only. There was no option to cancel or upgrade a subscription. Customers had to reach out through our customer service channels. Only after a few dozen customers had expressed the need for specific functionality, I would implement it and release it. Bit by bit, plan upgrades and downgrades, cancellation, and invoice-related functions appeared in our product; I only ever implemented these features when it started taking too long to solve these tasks manually.
When I released a feature that was supposed to show every customer a few statistics about their usage of our product, I didn’t think that this would impact our service much. After all, what were a few additional requests to the database? All of a sudden, my monitoring tools started bombarding me with notifications; my phone rang from the robocalls that I had set up when our system became unresponsive.
What had happened? It turned out that while I tested my statistic-collection logic on my local computer on a test account with a few dozen items in the database, the performance requirements of the database queries increased exponentially with the amount of data in a customer’s database. All of a sudden, hundreds of teachers with tens of thousands of items in their databases were refreshing their websites, triggering an unstoppable avalanche of requests to our database, which promptly locked up and didn’t respond to requests anymore. Our service went down, and when it returned, the renewed onslaught of database queries caused it to break down again.
I immediately understood what was causing these performance issues. I had to turn back time. I had to get the service back up and try to keep the downtime as short as possible.
This downtime lasted for not even two minutes, because I had foreseen something like this happening, and had designed our release infrastructure to allow for instant rollbacks. With one command, the previous version of our software that didn’t contain the statistics feature was switched back on, all traffic was routed to that instance, and within a few seconds, the database had enough breathing room to start working correctly again.
Never release something you can’t roll back: you can’t know for sure that your code is entirely error-free. In particular, when unexpected user behavior turns a benign feature into a resource-blocking monster, you will need to get back to the last version of your service that worked. The more automated this process, the better.
Three main things are important to create a system capable of automated rollbacks: artifacts, versioning, and bidirectional migrations.
Package your releases into easy-to-deploy artifacts. An artifact is a bundle, a package that can be easily copied and run on a server, either as some sort of executable or as a container that systems like orchestration systems like Kubernetes can manage. The idea here is that everything is well-specified, and you don’t need to move around files or assets. Usually, this involves a build process, where your whole application gets compiled, optimized, and packed up so that a final artifact can be created.
Artifacts are usually idempotent with the sources that created them: compiling your artifact from the same code twice will result in the exact same artifact. That means that if you want to roll back your service to the prior version, you don’t need to do any compilation again, you can just reactivate the previous artifact.
Version your release artifacts. You can reliably roll back to a version that worked if it has an easily discerned version number. If you just released v1.2.5 of your service and it breaks all of a sudden, you know that activating v1.2.4 will get your system back to stability. Versioning is a peace-of-mind activity, and many orchestration systems require it to distinguish artifacts.
Synchronize your database using bidirectional migrations. Imagine you have to make changes to your database with a release. You need to change the name of an important field in your user table that is related to your authentication flow. Your release goes through, changes the name of the field, and minutes later, you need to roll back the service to a prior version. If you don’t have a way to revert the change you just did in your database, likely, your service won’t start. You’ll have to frantically correct the change manually. Under stress, this can lead to errors that you might not be able to recover from.
Having migration logic in place that can go both ways is a way to make releasing database-related changes a very safe endeavor. Many web development frameworks like Ruby on Rails and Elixir/Phoenix have this feature built-in, but you need to know how to use it. Making bidirectional migrations a part of your release flow from the start will save you from a lot of potential trouble later down the road.
Automate the process. You will benefit immensely from removing manual steps from the release process. Continuous Integration, the concept of automating build and tests, makes releasing extremely easy and manageable. This level of automation that makes your business more sellable as well: if a developer you hire (or the one that replaces you after you sold the company) can release a new version of the software at the push of a button, this will net you a premium when your business gets acquired.
To be blunt, an automated process removes the one component that might mess up more than anything else: you. If you are required to execute steps manually, you might forget important parts, and risk bringing down your service with a botched release. An automated build system allows you to have sanity and integrity checks, stopping any dysfunctional artifact from ever reaching your production system. It’s one less thing to think about.
Release when it’s a good time. Don’t release at the time of day when most of your customers are using the product at the same time. Often, there are a few times a day when your traffic is lowest, and those are excellent release time windows. “Don’t release on Fridays” is a famous saying in the industry, as any corrective work will bleed into your weekend. Make sure you have the time to potentially roll back or hotfix your release.
Consider slow rollouts and feature flags. This might be an advanced approach to releasing, as you will likely not need this for the first few versions of your product. But it makes sense to at least consider building feature toggles into your product: a way for you to activate and deactivate access to certain features for your customers. Feature toggles will allow you to tie functionality to subscription levels eventually, and it will also give you a way to deal with emergency performance issues. I would have loved just to turn off the Statistics feature that caused my database to break down instead of having to roll back the release. Slowly rolling out the new version to our customers instead of just releasing it to all of them at once would also have given me a muss less stressful time figuring out the performance issues. I started implementing these things shortly after having experienced the Statistics fiasco.
Releasing and TMI: How Much Do You Make Public?
While we looked at releasing from the engineering side until now, let’s finish with the customer-facing perspective. Your customers can find out you released something through two means: they stumble upon it in your product, or you communicate the change to them in some way.
For many small changes, you won’t need to inform your users. If you were to tell them every single day that you moved a button a few pixels to the right or added an image to a block of text, they would soon feel overwhelmed. Save your announcements for the significant, impactful features that you want your customers to be aware of. Blog posts and newsletters are great places to communicate those.
While product update information has a “push” character, as you inform your customer directly, you also have the option of a “pull” source of release update information: offer a changelog. Changelogs can be shown to customers in the shape of a notification button, which they can click to see what has been changed recently. For technical audiences, this makes a lot of sense. For others, it only works when it is informative and helpful. Many customers only care about things that will indeed affect them. Don’t spam them with the minutiae of your product development. In the end, customers don’t have the attention span to read up on all of your many changes: they use your product to solve their critical problems. A good approach to releasing allows for that, and it will make sure they continue to be able to use your solution to have one less obstacle in their day.
How to Get to a Simple but Effective Release Management System
Your requirements for release management will vary wildly on the fidelity of your software. For example, if you run a SaaS with a web app, a mobile app, and some substantial background computing, you will likely need to release the backend and the frontend code and artifacts separately, involving a lot of automation very early in your products life. If you run a web-based SaaS without any extra fluff, you will likely be able to set up a simple release pipeline and be okay with it for a long time.
As long as your setup allows you to release early and often, you’re fine. The moment you notice yourself doing something that can be automated, integrate it into your release management system.
When I started developing FeedbackPanda, I ran the prototype locally on my development machine until it had all the necessary functionality. Very quickly, I containerized the application using Docker. That allowed me to fix the versions of all included libraries and runtimes, making builds more testable and reproducible.
In the beginning, I had all the configuration hard-coded into the application. There was only my local database, and all the API keys for the service were unchangeable once the build was running. This was fine for development but was keeping me from deploying the software to the public. Making this configuration part more flexible took some time, and it also means that certain adjustments needed to the code of the application to be able to inject secrets (like the database username and password) into the application from outside of the container.
The extra effort paid off, as a Docker container can be run on any cloud provider that supports containerd, which any major player does. That gave me a lot of options when it came to picking a hosting provider.
I would still build the Docker images locally on my development machine, and upload them to a container registry. This was fine until I was working from a place with very low bandwidth and shaky connectivity. At that point, uploading a Docker image could take more than an hour, which is an eternity if you want to deploy a hotfix for a bug that just appeared in production. Very quickly, I automated away the build step into a cloud build service, which both made it faster and less dependant on me being connected to a high-bandwidth line to deploy. Since then, I have pushed a few new releases from moving cars, trains, and even airplanes.
Build the release automation that enables you to release whatever you need, whenever you want, always allowing you to revert to the last working version.