During my first years as a Software Engineer I consulted for firms of different sizes (from 20+ to 5000+ people) and it exposed me to various levels of over-engineering. This can happen for a lot of reasons and is usually a collective issue that tends to get worse as time goes on.
This short list goes into some concrete(ish) examples of pitfalls and how to avoid them. Of course, this list in non-exhaustive and can be improved upon, so feel free to let me know if I’ve missed any glaring signs of this issue !
The Pareto Principle states that 80% of the consequences come from 20% of causes. In our case, the majority of our coverage will be done with a limited amount of tests, and the remaining percentage might take a lot of effort for little reward.
I recommend reading about the ISTQB testing principles, the second of which states that exhaustive testing is impossible.
One of the most important skills a testing expert possesses is the ability to identify the most important functions to test.
Have you ever been confronted with a bug in production, felt the rush of adrenaline while trying to fix it as quickly as possible and not being able to pin point where the exact source of the problem is? Did you have to go through layers upon layers of files, wondering why previous authors (yourself included) hid away details by placing them in some external function, four layers deep?
This is not as much of a problem when building a feature, as you’re focused on creating code that is clean and reusable, but it requires a lot more mental gymnastics when trying to understand it when you’re not the original author. Over abstraction can create unmaintainable & untestable monoliths, that’s why I believe writing concrete code first and then abstracting is important.
If you ever review code and can’t understand the underlying business logic within, that should already raise a red flag.
Always remember the Rule of Threes: an abstraction without at least three usages isn’t an abstraction. Good abstraction are extracted and not designed.
Micro-frontends (dividing parts of your application into smaller, self-contained units) can be beneficial in terms of DX & performance, as it:
- Allows you to only build the relevant part of your application in your CI, making it faster
- Allows you to ship parts of your app using different tech stacks
- Allows to load only the relevant files to your client, making your application more performant
- Allows teams to work independently in a "standalone” version and iterate faster on features
- And many more advantages I won’t get into ! I recommend this article if you want to dive deeper.
With all these benefits, why would it be a bad idea to use this architecture too quickly?
- Can add complexity and little reward: Ask yourself: what are the concrete benefits to using them for our project specifically? Does your project have a lot of independent moving parts?
- Can add little to no benefits for a lot of work: Setting up an MFE takes a lot of configuration & work side-by-side with your infrastructure team.
- Can increase the size of your payloads: if you have a diversity of technology stacks used, it may end up slowing down your user’s experience. This can manifest through duplication of common dependencies - let’s say two of your MFEs use React, you’d then have to download the dependency twice. A solution to that issue would be to use the same version of that dependency across your MFE, but that would introduce a form of build-time coupling of our MFEs which goes against a huge benefit mentioned above.
- Makes it harder to test your features: Here’s an example: say a banner on your homepage appears only when the user navigates to it from their profile, if both of these pages are individual MFEs, then you’d have a hard time reproducing that behavior locally as you’d probably be working on a standalone version of any given page.
The general ideas here also apply to micro-services, but I recommend this article if you want more information on the subject.
The YAGNI principle (created as part of Extreme Programming by Ron Jeffries) states that a feature should only be developed when required and not by anticipation. The main point being that developers should not waste time on creating extraneous elements that may not be necessary and can hinder or slow the development process.
If you try to future proof your code all the time, you’ll end up more often than not with unused code gathering dust in your repository.
Let’s say you need to declare a class
User that has a method
getAllUsers, you might start thinking that you’ll eventually need
getUserByEmail and code them right away. But that’s when YAGNI comes in - you should probably reconsider it and only code it when a feature requires it, and for a couple of reasons:
- The requirements might change in the future and make you update already unused methods
- The methods might simply never be used and take up space for no reason
Just like other programming principle (KISS, DRY etc.), YAGNI is pretty straight forward
This list was inspired by this tweet from Cory House, I recommend going through it as it'll show you some other mistakes that can cause over-engineering.
Have you every been faced with obvious over-engineering in a previous project or jobs? I’d love to hear some of your stories !
Thanks for reading, I hope you got something out of this list 😄. And feel free to follow me on Twitter, I'm always happy seeing my circle of dev friends grow 🥰