I used to complain a lot whilst doing my job. Not because things were actually bad or worth complaining over, but because my ruthless perfectionism caused me to notice things that I knew could be improved, and, to their dismay, the whole team had to hear about it.
This lead to some lengthy discussions surrounding the possibility of changing parts of the system. One good example was the state management in a React app I was working on. This was my first production app so with fresh curiosity and perfectionism abundant, I drew everyone's attention to what I thought to be obvious: we weren't using Redux.
Not only that but the app was making use of dozens of what were essentially singletons, each with their own method of managing state. It was similar to MVC, where a set of controllers expose the model to the view, allowing the view to manipulate the underlying model.
This is quite a well known pattern and remains a decent approach for many projects. But having just learned about Flux and Redux, I had a new found energy for pointing out that things would be easier if we used another abstraction.
What I failed to realise at the time was two-fold:
- I didn't realise that shipping new features at the time was more important than finding the perfect abstraction and
- I didn't realise that introducing a second abstraction would actually make things harder for everyone.
I'll circle back to the second one later, but let's focus on why shipping features might have been the right call at the time.
Every team has its trade-offs and I cannot stress how important it is to know which ones are currently being made. Sometimes you just have to ship functionality - it just has to work. Sometimes users are frustrated with a certain bit of the app and the UX needs to be improved. Other times the technical debt is so high that it's actually slowing down addition of more features and is making it impossible to fix bugs.
Whatever the current situation, trade-offs need to be made to rebalance the scales and stop the project from toppling over.
During this project, the main target was growing the user base - a familiar one for any B2C app. Secondary to that was improving the UX of the app because a lot of users found it confusing relative to other similar apps they were using.
Although I didn't realise it, to come to the table with the problem of the app using the wrong abstraction was probably the least productive thing I could have done at that point. But I was convinced it would help us debug the app more easily and implement new features more quickly, so I got buy-in from a few colleagues and started merging changes that were written differently to other parts of the app.
I justified the changes to myself, "it can't be worse to add an abstraction that adds structure - other parts of the app have no structure!"
This brings me to the second thing I had failed to realise. Abstractions increase complexity. When something is made generic, a tradeoff is made where uniformity is bought in exchange for rigidity and potentially hidden complexity.
What I thought I had done was to slowly start moving the app towards a more generic, easier to understand place but what had actually happened was that I had introduced a new concept that was actually incompatible with existing parts of the app. It even required extra complexity to be compatible with the layer in the stack that made API requests.
Some new features could be implemented with this new abstract and that's what I would try to do. My colleagues agreed with me that the code was easier to read and they bought into the idea as more features were developed this way.
But then we had to debug them. The added complexity that we acquired in the tradeoff bit us when bugs arose in the parts of the app using the new abstraction. This obviously was not great for morale and it was all too easy to blame the abstraction itself (Flux/Redux) over the actual problem - increased complexity.
My goal was actually to decrease complexity and the irony wasn't lost on me; I learned a few lessons as a result.
Untangling such an architecture would take more than a new paradigm to shift it away from unscalable "ball-of-mud" territory. I think the real problem was probably the API and the fact that it required a sizeable, stateful SDK to manage all of the client-side objects. This left the view and the controllers highly coupled to the SDK.
In hindsight, it's possible that with a lot of pain the app could be rewritten using React hooks. I remember a lot of the controllers in the app were essentially complex hooks (minus the abstraction now built into React itself).
But of course, this might not actually make things better - certainly cleaner but not necessarily less complex or easier to debug.
- Nothing exists in a vacuum. Understanding the current trade-offs is crucial.
- Buy-in is not always worth it. If you have to work hard to convince your coworkers of something, back it up with concrete evidence (e.g. a POC) and make sure you've convinced yourself first that you're really solving a problem.
- No framework/paradigm is a silver bullet. An abstraction will likely increase complexity.
- Multiple competing paradigms might be more confusing than a single one that isn't strict.
To that end, remember to know your trade-offs and hopefully you can make sure that your team is at least making them consciously and planning on rebalancing the scales at some point.