There is a special kind of pride that comes from the exhaustion at the end of a hard day’s work. Whether we spend our days laying bricks, pouring concrete, mowing lawns, cooking hamburgers, or smashing rocks; our exhaustion is proof that we’ve done work. The well-earned rest after all that work feels good.
What if our goal wasn’t just to smash rocks, but instead to find some tiny nugget of value inside just a small fraction of the rocks? With that goal in mind, does it still make sense to spend our days smashing every rock we see with a hammer, or is there a better, more focused approach?
As developers, we are smashers of rocks, searching to find tiny nuggets of actual productivity in a vast quarry, but yet we still smash on, blind to our waste because the hard work feels good, and we can pretend that exertion is accomplishment. We confuse effort with productivity, and because we value effort more than results, we actively rebel against things that might make our lives easier. Less effort means less work, which means less exhaustion, which means less pride in our output, and no one wants to devalue their own contributions. To do so would sour the sweet taste of the exhausted pride we feel at the end of the day.
We’ve become addicted to work for the sake of work. Not only have we become apologists for our own complexity, but we actually thrive on it. Suffering under the weight of complexity is a badge of honor. The more involved and convoluted our tools and solutions are, the more raw energy we expend per day, the more pride we feel.
My career is full of stories like this one:
My team had been tasked with creating an application made up of a few standard services as well as some batch processing and work dispatch components. In total we ended up creating 6 so-called “micro” services. Regardless of their purpose, each one was a freestanding process with an internal makeup similar to the following diagram:
Each of the services were stamped out of cookie cutter templates designed to simplify our interaction with boilerplate that dealt with non-functional requirements (the gray boxes in the preceding diagram). By the time we’d built one service, we’d learned things that forced us to go back to previous services and make changes.
We probably spent a few days on the core business logic, and literally months dealing with all the surrounding ceremony and converting our prototypes from services that worked on our workstations to services that worked in production.
This wasn’t an isolated incident. The next three applications I worked on all fell into the same pattern. We would copy and paste the boilerplate from a previous microservice, fill the business logic into it, and then spend the vast majority of our time working on the non-functional requirements.
Looking back across all of these deployments, at multiple companies for dozens of applications, the reason for change was almost never a change in the business logic or feature code, but all of the other cruft and barnacles that attach to, and bog down, the real code.
The duplication, waste, and boilerplate wasn’t the worst of it. The worst part was that for a long time, doing this felt good. I’d spend day after day typing and typing and churning out tons of code. At the end of the day I would come home exhausted, and I felt great pride this in exhaustion. For if I was exhausted, I must be working hard, and hard work is something to be proud of.
It took several trips down Deja Vu Avenue for many different applications before I started asking myself, “Should we really be writing all of this code? Are we writing the right code?”
In retrospect, there was only a small part of the code that I wanted to own (the green in the diagrams). It was all important (non-functional requirements are requirements after all), but as developers we only wanted to own a small subset of the code. Everything else could be solved elsewhere.
The conclusion I came to is that we should consume all of our dependencies as services on the other side of a contract. Instead of tightly coupling to every one of our non-functional requirements (NFRs), and having those NFRs weigh on our shoulders like Jacob Marley’s chains, we should treat everything we need as a service behind an abstraction, as shown in the following refactored diagram:
With an architecture like the one in this diagram, we can write a “pure” piece of business logic. Because it is loosely coupled from all the NFRs, we can deploy this component anywhere we like, run as many copies of it as we like for scale and resiliency, and we only ever have to push an update when the logic changes.
As a co-founder of Cosmonic and the creator of the CNCF open source project wasmCloud, I have a strong opinion on how we can implement this kind of loose coupling. We can leverage the portability, security, speed, and even limitations of WebAssembly to let us encode our features as WebAssembly modules and let the host runtime deal with the rest of the important things that we don’t want to own. Linking across contracts to our dependencies lets us focus squarely on writing our component; the right code.
While individual solutions like the wasmCloud runtime present just one solution among many, what we really need is a fundamental shift in mindset. As an industry, we need to stop placing such a high value on arbitrary exertion and instead value the right exertion.
We should be ruthless in our zero tolerance policy toward unnecessary complexity and superfluous tight coupling. We need to stop placing a higher value on raw effort than we do results, and ultimately we need to develop a self-awareness that allows us to recognize when we’ve become apologists instead of proponents.