#40 Step Three of Evolutionary Architecture: Focus on Growth
Your application's success might lead to handling massive user loads in specific modules. This is where the growth step of evolutionary architecture comes in - helping you solve such issues.
After exploring simplicity and maintainability in my previous posts, let's tackle the next big challenge: growth. What happens when success strikes and your app suddenly needs to handle hundreds of thousands or even millions of users?
Think about concert ticket sales. Let’s take Taylor Swift concerts as an example. Or if we hop in a time machine, imagine the chaos of The Beatles fans in 1964 or Queen enthusiasts in 1982 rushing to buy tickets at once. In these scenarios, certain parts of your ticketing application - like the ticket catalog, checkout, and payments modules - get slammed with traffic. However, other modules sit mostly idle.
This is where a modular monolith can start showing its limitations. Sure, you can scale up by adding more instances, but there is a catch: spinning up new instances takes precious time, and you are forced to scale everything, even the parts that don't need it.
Plus, some modules in your application might need frequent updates while others remain relatively stable. Yet with a modular monolith, every small change in one module means redeploying the entire application - even the modules that haven't changed.
When your application's growing pains start affecting both your team and customers through slower performance and frustrated users, it is time to take action. Let's say your ticket catalog module becomes the bottleneck - from time to time fans can't even browse available events.
What could be a solution? Extract the problematic module from your modular monolith and turn it into its own independent deployment unit. This could be a microservice, a nanoservice, or any other self-contained deployment unit. By separating it, you can scale and modify it independently from the rest of your application (of course, if there is no tight coupling between them).
Once you have separated the ticket catalog, you gain precise control. Need to handle a surge of fans browsing concert tickets? Scale up just the catalog service. Making updates to how tickets are displayed? Deploy changes to the catalog alone - the rest of your application keeps running untouched.
This separation comes with a trade-off: as your system becomes a distributed one, your services now need to talk across network boundaries which means higher latency and exposure to network errors and partitions.
You can no longer rely on simple internal communication methods like direct references, facades, or gateways that work when everything lives in the same application.
Instead, you have two main options:
Set up a message broker like RabbitMQ to handle communication between your ticket catalog and the main application. This is usually the recommended approach, as it keeps your services loosely coupled and more resilient.
Have your main application call the ticket catalog's public API (like REST endpoints). While simpler to implement initially, this creates a direct dependency between services and might make your system more brittle.
Keep in mind that not every application will face these challenges. Many systems will run perfectly fine without ever needing to handle massive user loads. And even if your application does experience significant growth, scaling your modular monolith horizontally might still be the simplest and most effective solution.
Don't rush to break apart your application into a distributed system just because it is a trending approach - make architectural decisions based on real needs, not hypothetical scenarios. The goal is to find the sweet spot between scalability and complexity that matches your actual requirements.