Most of the software I’ve built in the last 10 years is no longer being used for anything. It’s dead code that was useful a few years ago and has otherwise died on the vine.
It hurts to throw away code. It represents hours of my life gone — usually the hours between 10pm and 3am when my family is asleep and I actually have the mental room to work continuously for more than 5 minutes at a time.
After a few years of doing this, I came to an interesting realization. A realization that’s changed how I write code.
Software is Disposable
And it should be.
Software represents a solution to a problem at a particular time. As time moves on, so must the software. Some of it evolves, while a lot of it is replaced. This is part of the reason why Software Engineering will be a very stable and secure job for the foreseeable future, but as an engineer that considers code an artform, it required a complete paradigm shift for me.
You can see this truth in modern popular software architectures. Microservices architectures are popular because they represent a unit of code that can be created and tested, yet thrown away when needed. It represents a small enough unit of code that it can be easily written and tested, and there’s very little pain when it’s time to evolve or trash that small unit of code.
In many ways this makes a lot of sense. Consumer electronics and vehicles have been using this methodology for years; the core structure of the system is engineered so you can dispose of parts when they’re no longer useful.
This disposable software also has an interesting effect — it blurs the line between prototype and production. It’s pretty widely understood in the startup world that your prototype may very well make it to production. There’s no time and, assuming you’re building your business before funding, money to build code that gets thrown away outright. So even unsound prototypes can make it to production in some cases. This has been problematic for many startups as they attempt to scale up — but there are countless others that never made it to scalable because they took too long and didn’t just ship the prototype.
However, there is a middle-ground. Making software components replaceable means that a prototype is just a step in the evolution of the system. By designing software architectures that provide functionality in small components, and by making the way that developers connect to those software components flexible, you can realize a prototype that evolves seamlessly into production.
To solve the scalability problem, these small software components must also be repeatable. Luckily, small and replaceable parts are by their nature also repeatable. A small amount of load-balancing logic can allow a single service to be repeated multiple times, and provide rapid scaling response to even the earliest software system.
By designing these systems to be repeatable and replaceable, we realize another benefit to this:
Prototypes Can (and Should) be Scalable
There are many startups which have died on the vine due to technical problems and scalability issues (including market scalability issues), but even more have died due to missing the market opportunity. Software-based businesses should move quickly, but also be architected well enough to become the production system. This relates directly back to the first part of this story: That prototypes very often must make it to production.
Each startup has a critical engineer: The CTO, the VP of Engineering, or someone else early on that has a huge amount of influence in the technical direction of the prototype of the project. Having been that critical engineer on several occasions, and having spent time with many other critical engineers, I’ve come to realize that we have an important duty to ensure that our prototypes are scalable and our code is disposable.
How Do You Write Scalable Prototypes and Disposable Code?
There are a few key principles to writing scalable prototypes and disposable code effectively:
Separate the Interface from the Implementation
Most web frameworks directly couple the way that you connect to them (ie: the “route”) with the code that executes when you call that route. This is a tightly-coupled interface and implementation, and if you’re a trained software engineer that phrase should make you cringe.
The first step is to change the way your application interacts with the outside world. Tools such as node-red, noflo, and the Amazon API Gateway allow developers to design API’s as an afterthought to the design of the system.
Create be Small, Specific, and Tested Implementations
The core code of your application should be done in small pieces that are thoroughly tested. Small components are easier to replace and faster to write, and an architecture that allows components to be added later is robust enough to allow future development and expansion.
Small, independent services that perform limited functions can also be measured and scaled more easily and quickly. If one particular component of the system starts doing the heavy-lifting, adding more capability is simply running a few more instances of that service.
Automate Those Tests
Whatever your stance on Test-Driven Development, testing is a vital part of creating and maintaining code. Build small services with only a few commands per service, and always accompany those services with automated tests of some kind. The effort to create the tests (which is really fairly minimal) are nothing compared to the effort it’ll take to debug an intermittent regression if you don’t.
There are literally a million test frameworks out there, so pick one that works for you and your chose platform and test the crap out of it.
In the next few posts, I’ll be walking you through the process of building scalable, disposable prototypes using the toolchain I’ve developed (called Toccata). Toccata uses the following tools in the following ways:
- SenecaJS: Provides the microservices framework which does the bulk of the heavy lifting
- node-red: Provides wiring between API’s and MicroServices (through HTTP, WebSockets, SMS, and whatever other interface your heart desires)
- docker, docker-compose, docker-machine, docker-swarm: Creates containers for each service so they can be easily duplicated, scaled, and deployed across multiple hosts.
- mocha and unirest: Provides a testing framework for MicroServices and API configurations
- yeoman: Generates scaffolded projects based on previous successful architectures and systems.