How to choose the right software architecture for your business?
Software Architecture Styles
One of the most important decisions in each engineering team is to choose the right architecture. Wrong software design decisions caused many startups to fail, while at the same time picking the right system design gave a huge advantage over the competition to many successful companies. System architecture will affect how fast you can release new features to customers, how often you can do changes, developer productivity, resilience of your system to errors or downtime, organisational structure decision,… As your business evolves and modifies over time, your architecture needs to adapt as well — designing system architecture is a constant process rather than one-off thing.
On Diagram 1 there are presented 5 different software architecture models ordered by a high level system complexity — going from the left to the right part of the diagram system the complexity grows:
- Monolithic system is system that is integrated as a whole, designed to be self-contained. Monolith components are tightly coupled, they have single build and deployment unit.
- Modular monolith is a monolith where the codebase is split into separate modules with clearly defined boundaries. It is important that the database follows the same segregation.
- Synchronous microservice architecture consists of multiple tightly coupled components (deployable units with their own databases) which communicate with each other synchronously (usually using HTTP API).
- Event driven microservices are services which communicate with each other via event broker. They are loosely coupled.
- Serverless architecture consists of many smaller serverless functions which run on some cloud platform (AWS, GCP,..). They can be grouped into services.
Factors which affect architecture style decision
There are different segments that influence software architecture choice. In this article we will go through 6 main ones and use a practical example of Hurdle how those aspects can be combined to find ideal resolution for your business. Those factors are:
- Team experience
- Team size
- System availability
- Load Scalability
- Organisational Scalability
- Functional Scalability
Team experience
Complexity of your system dictates the knowledge your team needs to possess in order to build the system properly. More complex system requires more components which need to interact with each other, synchronise the data between different databases — dealing with eventual consistency etc. All those things require more knowledge, experience and time to get it right. If your team does not have experience with complex systems, monolithic architecture is probably a better option for you.
In very early startup days, usually teams choose architecture which mostly fits their knowledge and this is a fair compromise. As the team starts growing, other factors become more important.
Team Size
The more services the system has, the more people are required to maintain it. For example, building new backend feature in monolithic system requires only one repo that needs to be modified, which can often be done by a single developer. On the other hand, with an event-driven microservice architecture there are often multiple repos that need to be changed, usually multiple API contracts and databases which may be affected as well.
System Availability
One of the advantages of microservices over monolithic architecture is resilience to failures. Monolithic applications with solid infrastructure under the hood (multiple instances with autoscaling in place and CICD pipeline which supports blue green deployments or Canary release) can still have high availability. However, some of the things which limit monolith system availability are complex deployments with database migrations which require downtime — in those cases the whole system needs to be unavailable until migrations are done.
The more granular the system is (with multiple microservices and accompanying databases), the consequences of risky deployments are reduced and data migrations which require downtime are less likely since such migrations should affect downtime of some part of the system and not the whole system.
Load scalability
Load scalability determines how many requests the system can handle. There are 3 ways how the system can scale the load: horizontally, vertically and diagonally.
Diagram 3 shows how number of API requests might affect response time. Ideally, as the number of requests keeps growing we want to have constant response time for each of those requests.
Monolith systems usually have one database which is a bottleneck in order to scale the load horizontally, which leaves us with a vertical scale as the only way to scale effectively. On the other hand, with microservices — data is split across multiple data stores so a database bottleneck is not an issue anymore which gives us a plenty of options to scale both vertically and horizontally.
Organisational scalability
Organisational scalability defines how much you can get out of multiple engineers. With microservice architecture designed well enough it is much easier to support organisational growth than with monolith one. Even with perfect monolith architecture there are still some constraints like single codebase, single deployment at a time or single database schema.
Diagram 4 presents how growing number of engineers in organisation can affect developer’s productivity. Usually in organisations when the number of engineers start growing, developer productivity starts falling down. Ideally we want to at least keep developer’s productivity on the same level or make it even higher.
Functional scalability
Functional scalability is the ability to enhance the system by adding new functionality without disrupting existing activities.
On Diagram 5 it is shown the correlation between a number of features the system possesses and the time needed to develop new features. If we use an average time to ship the feature to production, ideally through time and with increased number of features our aim is to reduce time needed to ship a single feature.
In early startup days, when a team is still small and a product is simple with small number of features, adding new functionalities is much easier with simpler architecture — monoliths. In later company growth phases, as number of supported functionalities keep growing, it is getting more difficult to add new things to the monoliths. At that point, microservice architecture designed properly shows much more flexibility. One of the bottlenecks in order to functionally scale monolith is database – you cannot indefinitely extend single database schema without making it overwhelmed.
Hurdle example
At Hurdle we build Diagnostics-As-A-Service platform which offers our partners a wide range of health testing features. Our partners use our platform via our API to build their own services. Our product is in expanding phase, we are still spreading in new geographies and frequently adding new features. We experiment a lot, so we use Proof-of-Concept approach quite often by releasing PoC first and if we find more business traction we build the whole feature.
Considering our business model and phase of our product, there are 2 aspects which are the most important for us:
- System Availability. As a company with dominant B2B business model a high availability of our system is a key for success of our partners.
- Functional Scalability. We want to move fast, to release frequently and introduce new features to our partners as fast as possible.
In order to meet both priorities we defined above, we decided to use event- driven architecture as the optimal tool to achieve our goals and build our dream product. However, building PoCs using event-driven microservice architecture is slow, so we had to slightly adjust our way to reach our goals. We start new feature development with PoCs for which we use modular monolith architecture and then we slowly transition to event driven system. So, in most cases once we identify features we want to implement into our product our flow looks like this:
- Build and release PoC using modular monolith architecture. In this stage we try to reuse our current microservices as much as possible and extend them with a new domain. Code and database logic for new domain needs to be separated into its own module so in the later phase it can be easily extracted to its own microservice.
- Once we get enough business traction and we know the domain well (we do not expect bigger changes in the requirements soon enough) we build the whole feature and extract newly added module to separate event-driven microservice. This way we make sure we invest our time in the things that bring most value to our business but also not optimise our architecture too early.
By combining modular monolith and microservice architecture, we use the best from both approaches. Monolith gives us speed and flexibility in initial phase of our new features, while event driven microservice architecture gives us long term flexibility and stability of our platform. This way we have system which meets high availability standards and scales well across all 3 dimensions (load, organisational and functional) but at the same time is capable of delivering the most complex features as fast as possible.
Conclusion
Decision about the architecture style you want to follow will dictate many other decisions not only about a software, but also about the organisation structure, product and business in general. You need to consider various factors, such as the size of your company, growth plans, nature and the phase of your product. There is no single architecture style which can satisfy every business / product need. At Hurdle, we do our best to provide most advanced diagnostics platform to our customers not just by blindly following industry best practices but also adjusting them to our model in order to exceed even the highest expectations.