At the very end of last week, I jumped onto an Incident call in order to listen and learn about a part of the InVision platform with which I didn't have too much familiarity. And, unfortunately, as the investigation unfolded, I realized that part of the root cause of the incident related to a misguided architectural choice that I made 7-years ago in a completely different application. The whole situation got me thinking about the lifespan of our choices. And, in particular, about all of the lessons that I wish I had learned much much much earlier in my web development career. Lessons that could have prevented the Incident on Friday. Lessons that could have prevented many incidents and facilitated easier maintenance of my application code.
Each day, my primary goal is just to be better than I was yesterday. The journey of mastery in web development isn't a sprint - it's a marathon; an eternal marathon in which the finish-line is an ever-moving target. But, I enjoy learning new things and I hope that continues - it's what keeps this job so exciting. The following are some of the lessons that I wish I had learned much earlier in my career as the value-add of these particular lesson is comparatively great.
Please note that this is just a high-level overview. Each one of these lessons could be an entire post on its own; and, in fact, several of them are (see embedded links about database index design and error handling). The goal here is only to pique your curiosity and get you to think about aspects of web application development that you may not have top-of-mind.
Clever code is the enemy of long-term maintenance.
Honestly, this may be the most important lesson of them all. Clever code is a lot of fun to write. But, that fun often comes at a high cost. And, unfortunately, the impact of that cost is never felt when authoring the code. In fact, the uphill battle of writing clever code is exactly what makes it feel so thrilling! The true cost is only felt when such code has to be maintained in the future.
Clever code is usually doing something complex, unexpected, surprising, or overly terse. Because of this, clever code is hard to understand and harder to maintain. It becomes a part of the code that no one wants to touch - even the people that original authored it.
Strive to make your code as straightforward and as boring as possible. Boring code is stable code. Boring code is code that no one is afraid to touch.
Pro tip: If you ever justify your code with a statement like, "If someone had a deep understanding of the language / framework / technology / methodology, this wouldn't even be considered clever," then you're probably writing clever code.
Don't Repeat Yourself (DRY) addresses business logic, not syntax.
Don't Repeat Yourself is one of the most appealing mantras in all of web development because it seems like it should be easy to apply: after all, it's visual - if "this" thing looks like "that" thing, it's repetitive and should be factored-out. DRY'ing out code feels productive. It feels like you're adding value to the application, especially as a novice developer trying to find your way in the world.
Unfortunately, the litmus test of "repetition" is often applied to the wrong thing: syntax. In other words, the look and feel of the code. And because of this, things that are unrelated from a business standpoint are often grouped together based solely on their shape. This creates tight-coupling between disparate parts of the application which makes each individual aspect harder to maintain.
The "repeat" in "Don't Repeat Yourself" applies to business logic, not syntax. Business logic speaks to the invariants of an application - to its truths. When you duplicate business logic, you increase the chances that the truths in the application can diverge over time. As such, business logic should be centralized and invoked rather than duplicated.
But, take care to do this only as much as is necessary: that act of invoking centralized business logic is not "duplication" - it's DRY'ness at the appropriate level of abstraction. If you try to "dry out" the invocation of business logic - not just the codification of business logic - you will end up creating abstractions that are hard to reuse.
Write code that's easy to delete, not easy to extend.
This wonderful piece of advice is from Tef. And, follows naturally from the previous two lessons: if your code isn't clever and it applies DRY'ness at the correct level of abstraction, then your code will be much easier to delete. And, the ease of deletion is a strong indicator of its maintainability.
I spend a lot of time deleting other people's old code; and I can say from this experience that the code that's hardest to delete is almost always the code that's tightly-coupled, brittle, and hard to understand. If you write code with an eye towards its eventual removal, you'll be more likely to create code that adheres to a better separation of concerns and leads to increased clarity.
Pro tip: Easy-to-delete code also works incredibly well with feature-flag based development. Feature-flag based development leads to safer deployments and a more agile team environment.
Idempotency creates flexibility and robustness.
Idempotency is, very roughly speaking, the ability to safely perform the same action multiple times without getting an undesirable outcome. For example, an "INSERT IGNORE INTO" in MySQL could be part of an idempotent operation as it quietly ignores duplicate-key errors and allows the same SQL statement to be run over and over again.
To be honest, I'm still trying to wrap my head around the concept of Idempotency and how to best apply it in my web application architectures. But, from what I have seen and what I have discussed with the likes of Ben Darfler here at InVision, I am deeply convinced that an Idempotent mindset is a key component of a successful project.
The attempt to write Idempotent code forces you to think about failure modes and requires you to consider what happens when systems inevitably go wrong. This leads to logic that is more flexible and robust because it is less likely to ever leave an application in a "bad state".
Database indexing is not a dark art.
For the first few years of my web development career, I didn't even know that databases had indices. And, when I first tried one, it blew my mind - so much so that I still remember it vividly well over a decade later.
Once I knew that database indices created more performant applications, I tried to learn more about them; but, I had trouble finding information that I could easily understand. As such, I started to believe database index design was some kind of magic - something that few people knew much about.
Eventually, I came to realize that database indices are not magic. In fact, they're fairly straightforward. Not so long ago, I poured my heart into a write-up on this matter, "The Not-So-Dark Art Of Designing Database Indexes: Reflections From An Average Software Engineer". If you are curious about database index design, take a look.
I guess this one isn't much of a "lesson"; I just wish I had known about and understood database indices much earlier in my career. It would have enabled me to write much more efficient applications. And, it would have helped me to think about the difference between data-reads and data-writes.
Exceptions are for developers, error responses are for users.
Error handling is hard. It's something that I've struggled to "get right" for years. To date, one of the best insights about error handling came to me from Mark Seemann in his article, "Exception messages are for programmers". It was perhaps the first time that I saw someone clearly identify the separation between the errors that an application sees internally and the errors that it reports to its users.
In fact, I was so excited by Mark's post that I basically re-posted it in my own words, calling out the need for a "Translation Layer" within the application that manufactures user-safe error messages based on exceptions thrown within the application. This separation of concerns was truly an "Ah Ha!" moment for me; and, one that really brought much of my thinking on error-handling into focus.
Just as with database index design, I attempted to take all of my reading and learning on error handling and pour it into a post, "Considering When To Throw Errors, Why To Chain Them, And How To Report Them To Users". It reduces error handling to set of DO and DO NOT directives that help guide me in my application layering.
Caching needs to be done in the right layer.
Caching is both terribly powerful and terribly complex. In fact, cache invalidation (just one aspect of caching) is said to be one of the two hardest parts of computer science, right along with naming and off-by-one errors. The complexity of caching can be amplified if it's done at the wrong layer of the application.
This is - at least in part - what caused the incident that prompted this blog post. Well, caching combined with "clever code." I had a control-flow that was performing caching across two layers of the application. This caused caching to be used at unexpected times which lead to a mysterious race-condition which caused the incident.
There is no one "correct" place to do caching in a tiered application. I believe that the best place to do caching is closest to the domain of expertise; and that the knowledge of said caching shouldn't leak outside of that layer.
What I mean by this is that caching should be contained within the tier that knows when and how to cache. And, that the tier above it / below it shouldn't have to know if - or depend upon - whether or not the data in question is cached.
A good illustration of this might be a Service Worker in a Progressive Web App (PWA). Even if a Service Worker is capable of caching an AJAX payload, the application that's built on top of that Service Worker should never assume that an AJAX payload is cached; and, if the application needs caching of data, the application should explicitly cache the data in the application logic.
In other words, the application should cache data based on business rules (its domain of expertise) even if the Service Worker is already caching some data based on HTTP Headers and pre-loading strategies (its domain of expertise). To let the knowledge of the Service Worker behavior leak into the application layer is to create tight-coupling between the layers, which leads to code that is harder to understand, maintain, and debug.
To paraphrase Dicky Fox from Jerry Maguire, in web development, I've failed as much as I've succeeded. And with each failure, I've learned new lessons that paved the way for future success. In this post, I wanted to outline some of the most important lessons that I think I've learned - the ones that really burned me good and lead me to some powerful moments of clarity. Hopefully some of this has either struck a chord in you; or, at the very least, gave you some food for thought.