Abstractions are a critical part of software development. While we developers often abstract too much, software would take exponentially more time to build today without abstractions. Few write their own code to store data these days. We use databases instead. The same goes for web servers. And HTTP libraries. And JSON encoders/decoders.
Like anything though, we can have too much of a good thing. The point where we’ve over used abstractions usually involves the business logic of a product. When an abstraction is built there is an inherent assumption that nearly every possible use case is accounted for. For a pure technical abstraction like a database, this isn’t a bad assumption as there are decades of software projects out there to serve as example use cases.
For the business logic of a product, building an abstraction requires the assumption that you both know exactly what your users want and how the product will evolve in the coming years.
Those are enormous assumptions.
Although we can rely on previous experience to build technical abstractions, there is little to no experience to rely on for a business logic abstraction. What was the last copycat product that made you go “Yeah! This is amazing!” ?
Many new products that are similar to others usually have something different about them. There were lots of search engines available in the 90s, but PageRank made Google’s the best. Mastadon may look like Twitter, but being decentralized is a huge differentiator (whether it’s enough to make it as successful as Twitter is a different issue). There are lots of blogging platforms, but dev.to differentiates itself by being the fastest. I’ve made Maleega’s differentiator as the only email client that isn’t trying to help you get to inbox zero.
Copying another product without a meaningful differentiator does not usually provide success. Bing still lags Google (I’ve actually tried to give it a chance). Google+ threw in the towel to Facebook. Everyone has a hard fight against AWS.
That’s what makes abstraction of business logic hard. A successful product usually involves doing something different than existing products. In extremely rare cases can people guess what that differentiator is right away. The rest of us have to experiment to see what resonates with users.
Building abstractions without knowing the results of those experiments is a huge mistake, one that I’ve seen made countless times. It usually goes like this:
- Product is defined
- Abstractions are built for use cases A, B, C, D, and just to be sure: E
- Users start using the product and want use case F optimized
- Developers grumble at how the system isn’t designed for F
The best case here is that the team somehow muddles through and builds the product the user wants. Maybe there’s a refactor of the abstractions. Maybe workarounds are built. Either way, the abstractions that were meant to save development time in the future have actually acted as technical debt.
The worst case is that the product enhancements aren’t worked on because of technical difficulties, leaving an opening for a competitor to build a better one.
No amount of technical experience can make up for not knowing which direction a product is going to take. For this reason, it makes sense to put off building abstractions around the business logic until that direction becomes clearer. Patterns will emerge after a product has seen some use and abstractions can then be built to match those patterns. The best abstractions I’ve built have been refactors of existing logic. Some people hate refactoring, but I don’t think it needs to be avoided at all costs.