Clean Architecture in practice
Recently, at work, I encountered an issue while updating a feature to a new UI. It coincided with the data model change, resulting in a change across the stack. The first thought was that it would be easier to start over, leaving the old world untouched, and just focus on a new shiny implementation that would solve all the problems the old one had. As we all know, when you think it is a mess and you will need to start over, under release pressure, you’ll likely produce a new mess and end up in a worse position than you started. Fortunately, our app is nicely divided into layers and we are very good at testing both UI and unit, so I can safely change the code without the regression risk.
I decided not to go with the temptation and refactor instead of rewriting. Because of the cleanish architectural structure, I was able to refactor layer by layer, relying on the interfaces in between.
Since the data contract with the API was still in flux, I started with my domain. I knew what it should look like and what sorts of data I would expect, so I defined my domain models and use cases accordingly. I used the old contract to feed the domain with data and just refactored mappers to map to the new models. Mappers between the domain and presentation needed a minor update as well to support new domain models, but all were pretty cosmetic. This way, the domain was defined and prepared to feed my presentation layer. It could have been merged to master without any feature flag as a refactor without any functional changes.
On top of that, presentation and data could be built in parallel. I started with a presentation to give the API contract some time to stabilise. Since the new screen differed significantly from the old one, I decided to create a new UI behind the feature flag—a relatively straightforward process of building the view and feeding it with the data coming from the use cases. Once the domain was merged, this could be put into a PR and merged in without problems. It was working fine. However, some data was not populated correctly due to our continued use of the old API contract. Nevertheless, it was safe to merge since we were behind the feature flag, which meant we wouldn’t hit production immediately, allowing the designers to review the UI.
The last final step was to use the brand new API contract. Simply enough, I updated the graphQl schema and started consuming it. Since my new domain models supported both old and new views, I had to merge the updated data with the old one to ensure all necessary fields were available. With few @Depraceted annotations, I ended up with a nice solution that contains two data models that are mapped to one domain model that drives the old and new views.
That’s my recent story about how Dependency Inversion and Interface Segregation saved the day for me, and how the Clean Architecture with good test coverage helped me to be more productive and confident about my work.