Case Study: A Monolithic Software Refactoring
In this article, I just want to share a short journey about refactoring a monolithic eCommerce software. I will not too focus on how to use CQRS or DDD, because a huge software refactoring is not just about engineering but also people, decision, organization and culture. Just share my thoughts.
Background
I was an engineer in this team and responsibility is maintain a large Checkout system. It’s a B2C platform and already exists for more than five years and developed on Ruby on Rails. We were a small team, except me, the other engineers we were all new graduate and all of us involved Ruby development less than one year. This system was handover from the other team about six months ago.
This software is just a monolithic MVC structure, it means
- 6000+ lines in one controller file and have other controllers…
- 4000+ lines in one model file and have other models…
There’re still a lots of bad smells that I don’t want to write them out. In fact, this situation already introduced a lots of bugs and previous engineers just try to pile up their codes for fixing bugs or features everyday. Overall, the software quality is pretty low. However, this system still have huge orders per day!!
What’s Wrong?
After I took over this system, I started feel there are something bad:
- No Tiers: Actually, we have some ActiveResource or some other rails modules, but overall it is still a bloated MVC.
- No ACLs: Our system rely on other APIs to interact on data. We don’t have to connect the database. However, all the API responses or external data are spread to everywhere without any wrapped.
- Weakness Domain Modeling: Only few business logic is in the right place. Actually, you can find a lots of important logic everywhere.
- No boundary/Context: Just like ACLs. We have to avoid the core software is corrupted by any irrelevant things. We should consider this in any tiny thing. For example: You can find Rails Params or Routes everywhere.
- Bad Flexibility: Functions and Class have heavy dependencies. Hard to extend and lack of abstraction design. There’re too many fragment functions, some are over used some are useless.
Available Solutions
I think nobody want to contribute on this codes. The risk is pretty high when you do any change even the change is minor. So I started to think how we can overcome this bad situation?!
At that moment, I had a strong feeling that we probably can drop current system and rewrite a new one:
Keep maintain the legacy one and develop a new one at the same time
However, this solution is nearly impossible because below concerns:
- Resources: Maintain and develop new one is inefficient sometime
- Organization: Not all the managers agree this solution.
- Culture: Not everyone embrace the big changes.
There’re too many restrictions in the real world. So the question now will be: “How we refactoring on a monolithic software?“
Domain Driven Design, DDD
DDD is just a way to help you build a flexible software. Which not mean you can always get success when you adopt it. Before adopt DDD, I had to look around our team and current project status.
- Project Status: Only one month handover, all the engineers and product manager still can’t understand all the Checkout logic. This is because we don’t have a completed handover and lack of documents.
- Engineer Level: If you team member is not familiar with object-oriented programming, it will be a high risk when you adopt DDD. In our team, they don’t have enough object-oriented experiences on programming, design and thoughts.
Anyway, we can give our member a completed learning path to let them can master on OOP and DDD. However, it’s also too inefficient. In addition, DDD is not very easy to adopt it because it’s not just programming.
In fact, DDD is best solution except for rebuilding the whole codebase. But this path is too long and also we definitely need more domain and DDD experts to join.
A Progressive Solution with CQS
There were too many perspectives to be concerned when you refactor this software. Finally, the most concern is people and how you start. Following is just my step by step plan:
Add Boundary
In this monolithic software, the first thing we need to do is establish the boundary:
There’re two bounded layer being add in above diagram: ACL and Facade.
ACL
We have a lots of APIs which provided by other teams. However, after we get the response of APIs, we always return a plain Ruby object or a JSON data to the caller directly. It’s really a bad practice if there’re any changes on property name or type, because these response data already spread to everywhere.
It’s important to establish this layer for avoiding core service/domain being corrupted by other contexts. ACL have to restrict the API’s changes in this layer and return meaningful data(Wrapped class or Adapter) to the caller, for example: OrderDetail
or ShippingMethods
.
Facade
Facade is just a kind of context interface for Controller, for instance: OrderFacade
, ShippingFacade
. This layer is intended for avoiding the core domain models to know any details about Rails’s Controller or Param, Route etc. In this layer, every payload should be described as a DTO and the return value should be a VO, View Object.
Following is our mainly purposes for Facade:
- A boundary between controller and core model
- Integrate or construct multiple different processes in the Facade.
- Core service should be unawareness of any Rails frameworks.
Progressive Refactoring with CQS
CQS is a pretty simple pattern:
It states that every method should either be a command that performs an action, or a query that returns data to the caller
In our system, more than 95% actions is about query. For example, get shipping & payment methods, amount calculation, invoice, orders etc. This could be a reason to leverage on CQS. But mainly reason for our teams are:
- Easy to understand and implement
- Flow/Process division is easier
- Easy to become a standard for every engineers.
In the beginning, we choose one case to refactor(ex: Get shipping methods) and start to define the component and glossary as below:
In each refactoring case, we extract the domain models and made it more flexible as possible as we can.
This is a most easiest way to let every members to involve a case and also understand the business logic deeply.
Every time we refactoring a case, we are closer to with this system and it’s good also for designing the domain models. Even our member didn’t have much experiences on OOP but they still could learn OOP skills in each refactoring case. Not only level up the software but also the engineers.
Model Design with OOAD
This is the most hardest thing when we leveraged this solution, because we separated refactoring by each single case. It’s high risky to design the model in this situation. However, we still had a lots knowhow so that we can avoid to design wrong models at the beginning. Anyway, be careful about domain modeling, flexibility is first thing you have to concern.
Single Responsibility Principle, SRP is a important concept for me when design a class and Design Pattern can help you to plan a flexible design.
Extract a common logic to a single class or function is not real modeling. It will make your code unmaintainable and unreadable sometime.
Above just a feeling when I refactoring or modeling domain. Think following question twice or more when you extract a common logic. Because it maybe an important logic.
Where this logic should be?
In addition, keep in mind:
* Make thing simple instead complex
* Loose coupling and loose dependency are king rules
Conclusion
Software refactoring is not only a kind of art but also a hard skill. For me, the hardest thing are people and the first step, sometime you should pay more attention on it. Anyway, enjoy the journey and you will find the interesting parts.