Kirsten Westeinde, senior engineer at Shopify, discussed the evolution of Shopify into a modular monolith at Shopify Unite 2019. This included using the design payoff line to decide when to make this change, how it was achieved, and also why microservices were ruled out as a target architecture.
A key takeaway is that monoliths are not necessarily a bad architecture, coming with many advantages such as a single test and deployment pipeline. This is particularly useful at the start of a project when new features must be shipped quickly. It's only when the "design payoff line" is crossed, the point where bad design impedes feature development, that the architecture should start to be improved. In the case of Shopify, improving on their architecture did not mean a move to microservices, but a move to the modular monolith. This combined the advantages of a monolith, such a single test and deployment pipeline, with the advantages of microservices, such as code modularity and decoupling.
Westeinde believes that a monolithic architecture is a good starting point for a project, stating: "I would actually recommend that new products and new companies start with a monolith". She lists some of their advantages:
- A single project containing all the code.
- Due to this single codebase, testing and deployments are straightforward.
- All data is available and does not need to be transmitted across services.
- A single set of infrastructure.
Because of these benefits, Shopify started off as a small Ruby on Rails monolith, over time evolving into an extremely large codebase. As this happened, it meant that Shopify started to become unmaintainable, thus making it hard to deliver new features. For example, changing one piece of code would cause unintended side effects on seemingly unrelated code, and building and testing the application took too long.
Referencing the design stamina hypothesis by Martin Fowler, Westeinde explained that this is when it became time to refactor their architecture - once feature development is impeded by bad design, a design payoff line is crossed which means that it makes sense to invest resources to fix this.
Initially, Shopify looked at microservices as an alternative more maintainable architecture. However, they were ruled out due to the complexity of a distributed system, instead wanting a more maintainable monolith:
We realised all the things we liked about our monolith, were a result of the code living in and being deployed to one place. And all the issues we were experiencing were a direct result of a lack of boundaries between distinct functionality in our code
Westeinde explains that they realised their design goal was to increase the modularity of the system, such as with microservices, whilst keep a single deployable unit, like a monolith. In order to achieve this, Shopify adopted the modular monolith pattern. This allows boundaries between code, but for that code to live in and be deployed to the same place. The migration path included:
- Code re-organization: Initially the code was organised like a typical Rails application, with the top-level parts being named after technical components such as controllers. This was changed to be organised based on business functionality, such as "billing" and "orders", making it easy to locate code.
- Isolating dependencies: Each business component was isolated from the other, then made available to use through a public API. A tool named Wedge was developed in-house which tracks the goal of each component towards isolation. It does this by building up a call graph and then working out which calls, such as ones across components, are violating.
- Enforcing boundaries: Once each component has achieved one hundred percent isolation, boundaries will be enforced between them. The idea is that there will be a runtime error when code tries to access code in a component which it hasn’t explicitly depended on. Having dependencies declared in this way will also allow them to be visualized in a dependency graph.
To conclude, Westeinde explains that this was a good example of how architecture can evolve based on business needs:
Good software architecture is a constantly evolving task, and the correct solution for your app absolutely depends on the scale that you’re operating at.
The full talk can be watched online, also coming with a corresponding blog post.