Shipping fast
I’ve been lucky enough to work as an independent developer for the last year (Satyrn) and before that I spent 5 years running a small startup with a team of 8 engineers (Tactic). So I have not had to deal with a lot of the red tape that tends to slow things down at larger companies. But I think a lot of the lessons I’ve learned on shipping fast are universal.
Why it matters
If someone gives you feedback and you act on their feedback the same day and share the result with them and thank them for their feedback they are much more likely to give you more feedback. More feedback is usually critical in building a great product, so shipping fast not only has the benefit of building the product faster but it also means you will build a better product.
Acting on feedback quickly allows you to put something tangible in front of users which will spark new ideas that would otherwise disappear if you wait too long. Sometimes you can get this outcome by sharing design prototypes or videos, but nothing beats a working product.
Small changes are better than big ones
I’ve always respected engineers who can make the smallest change necessary in a codebase to test a hypothesis. This often requires some mental gymnastics when a more straightforward approach might involve a large refactor. Often it’s difficult to find a two-line “hack” which gets what the user needs into production faster, but I’ll usually prefer this approach.
Of course if you keep adding two-line hacks into a codebase it will eventually make it harder to ship fast. But in general I’ve got a very high tolerance for this kind of added complexity. I’m constantly surprised by how fast you can ship even when things get very tangled. That said I get a lot of satisfaction from well architected and clean code. But I prioritize speed of delivery and user value over tidy code.
I prefer to avoid spending more than a day at a time on refactoring for the sake of cleaning up code, instead I like to do small amounts of tidying at the end of every day when my brain is not in a creative mood. I’ll often keep notes on ideas for quality of life refactors and chip away at them during downtime. I’ve found that choosing the wrong level of abstractions can end up making your code look cleaner but makes shipping slower. For this reason I prefer to tidy incrementally and readjust as new requirements arise.
In some cases it’s clearly worth taking a little longer shipping a feature to do some refactoring ahead of time to help your feature velocity (and product quality) stay high. I’ve had a few instances where I’ve regretted taking the shortcut, in particular when they require a long-running data migration to “fix”. But that’s not a good reason to be scared of them, I’ve found it’s important to build a good muscle for running data migrations or infrastructure changes when necessary, just make sure you do some testing ahead of time to make sure its actually feasible.
Prototyping and incremental deployment
For the most ambitious projects it’s often unclear what architecture is appropriate and even what requirements are necessary. In these cases I like prototyping in an isolated project. Once I get the basic elements working in a toy project it’s easier to see how they will fit into the production system. Then if there are unexpected additional complexities while integrating I can simulate them in the toy project where they are easier to resolve. And if some large change to the subsystem is needed later, having the toy project to experiment on makes it much easier and faster to get things working in production.
Shipping fast is synonymous with shipping incrementally. If a quantum of value for your users can’t be delivered quickly it’s important to make changes under-the-hood incrementally. Often there are smaller seemingly unrelated pieces of value you can deliver which ladder up to the change you are gunning for. I’ve found this to be the case even when working by myself where I am the sole proprietor of the main branch.
Faster deployment = faster shipping
Nailing a simple and fast deployment process is critical to shipping fast on a psychological level. The lower the friction to deploy code the subconsciously easier it becomes to see ways to break down the work into incremental pieces. So the lower the friction, the more incremental you can ship, and the faster you will end up delivering value to your users.
Besides bad devops, the reason deployment becomes slow comes down to the testing and acceptance criteria, and sometimes this is unavoidable. Eg when your production scale is far beyond what you can easily simulate in a development environment, or when your simulations take a very long time to run. At Tactic we had a terabytes of data in our production database and “data bugs” haunted us constantly. We relied on long-running “smoke tests” on a staging branch and manual QA, and it was a constant battle to shift toward more and faster automation.
Unfortunately as an application becomes more complex there is a natural tendency toward longer-running and often flaky tests resulting in lower feature velocity. And unlike application-level tech debt there is more context switching required to optimize CI/CD and testing capabilities, therefore it’s best to give this work your full attention, rather than deprioritizing it until the end of the day when you’re tired. I find its best to dedicate an entire day (but not more than a day at a time) to improving deployment speeds periodically.
Where large optimizations are possible but they will take more than a day I like to split up the work, and refactor the CI pipeline or codebase toward the end state incrementally, interleaving that work between delivering features for users, because similarly to application level tech debt, new requirements will often arise that scuttle your perfectly laid plans to optimize.
Cutting red tape
Similarly, as an organization becomes more complex there is a natural tendency toward diffuse accountability and ownership which also results in lower feature velocity. The best way to counter this is by periodically resetting expectations where ownership and accountability are re-centralized. Even in my experience working at a small startup I could feel the gravity set in. As our customer base and product surface area grew, and our infrastructure became more complex, so too did the number of stakeholders involved in making smaller and smaller changes to the product.
I’m a big fan of empowering individual engineers to solve all their own problems and remove roadblocks. I love the “devops first” philosophy where all engineers are expected to contribute to deployment and observability systems. And I love seeing more project management, design, and product decisions getting pushed toward engineers. Clearly this only works when the project complexity is manageable and engineers can empathize with the customers, and that’s not always the case, but this trend is something I’m excited to see because it results in shipping faster.
The kind of work I enjoy
Shipping fast isn’t just about velocity – it’s about maintaining momentum and staying energized by the work. When I ship quickly I find myself engaging with users more frequently, their excitement becomes infectious, and their feedback leads to better product decisions, which makes the next iteration even more rewarding.
I’ve found that the most fulfilling work comes from striking the right balance between pragmatic shortcuts and thoughtful architecture, between solo ownership and team collaboration, and between speed and quality. While there’s no universal formula, the principles of small changes, incremental deployment, and minimal process overhead have served me well.
For those building products, I encourage you to examine where your deployment friction points are, where process is getting in the way of progress, and where you might be over-optimizing for code cleanliness at the expense of user value. Sometimes the fastest path to shipping isn’t the prettiest, but it’s often the one that teaches you the most.