From One, Many — Building a Product Suite With a Monolith
Aha! has now reached $100 million in annual recurring revenue with three separate software products in our available suite. We did this all without taking money and without breaking our monolith into microservices.
Monolith vs microservice
Modern web applications have used many different architecture patterns. Two of the most common are a single monolithic code base and a system of isolated microservices. A monolithic code base contains all of the code to run the entire feature set of an application in a single codebase. Monoliths typically connect to a single database to store all of the necessary data for an application. In a microservice architecure, the entire product functionality is split into many different services that all have their own code bases and backing databases. Each microservice is designed to handle small tasks and limit the functionality that is contained in a single code base.
We like to move fast
By utilizing a monolithic architecture, Aha! has moved from a single product with Aha! Roadmaps, to a suite of tools. Our engineering team was able to add Aha! Ideas and Aha! Develop as standalone products in less than 18 months. With all of the code existing in one Rails repository, it was easy to share the infrastructure, data, business logic, views, and user experience elements.
We maintain a single repository to run the entire Aha! suite of products on a local environment. This means that our engineers can have access to the full code base and all functionality — running locally on their first day. Once they have downloaded the repository, all development can be done without any internet access. There is no cluster of development services to connect to and no intricate system of virtual machines from separate repositories to manage. The engineer only needs the code delivered in the monolithic repository and the engineer's own system.
There are portions of our code that do live outside of the main Rails repository. We have some frontend tools and an integration engine that live in separate repositories from the main Rails application. However, all of these separate repositories are pulled into the main application as dependencies. These other repositories are necessary for building new features in those areas but they are not necessary to load up the entire application functionality locally. These separate repositories are also not deployable assets or services on their own. They are encapsulations of areas of code that we may want to open source or utilize in other contexts outside of our main production application. In fact, efforts have been underway to limit these ancillary repositories and our engineers continually lament having to work outside of the main repository to complete tasks.
We ❤️ Rails
Ruby on Rails has been monumental for the growth of Aha! and many other applications across the internet. Rails has been a consistent option for web development because of how easy it is to quickly generate a project. A very strong community has also grown around Rails due to its open-source nature. Both the conventions that are provided by Rails and the community surrounding it have been a major catalyst in our velocity at Aha!
Rails conventions provide a standardization that lowers the learning curve for new engineers joining the team. We don’t have to spend a lot of time explaining the architecture decisions for a large majority of the application because they follow conventions. It is easy for an experienced Rails engineer to navigate the application and quickly find the areas they need to focus on for a particular change.
With over 15 years of web applications running on Rails, the Rails community provides an immeasurable value to our application. The community helps by providing code in the form of gems that can be used to extend Rails and by generating mountains of material to learn and grow in the ways to use Rails. Blogs, podcasts, forums, conferences, books, and many more mediums are dedicated to Rails. This collection of material aids in our problem-solving by seeing the different ways that other engineers have solved the same problems with Rails.
Our Rails monolith allows Aha! to benefit from the work of thousands of engineers as if it was a much larger organization.
Complex code over complex processes
The image on the left shows a monolith where all of the functional blocks exist inside one single code base. Each of the green lines represent communication between different areas of the code inside of the monolith.
The boxes on the right represent individual microservices that have their own code base and data. Each of the blue dotted arrows in the diagram represents a contract between microservices.
The organization must become more complex in order to handle the information handoffs between teams that are managing contracts in a microservice architecture. The contracts between services have to be discussed, documented, developed, and maintained. This is typically done by different teams that are responsible for a limited scope of the architecture. Each of these lines become separate tasks that have to be analyzed and organized by product managers, business analysts, and team leads. These tasks may appear in different sprints in different workspaces across the software development tool. While each task is now simple, the complexity has shifted to the ways in which work and process are organized and distributed across teams. This leads to conversations about process, contracts, endpoints, due dates, and dependencies. We prefer our conversations to be about users and lovable interactions.
Engineers build better when they understand the reason they are building. It can be extremely frustrating to spend your time working away at a problem that does not have a clear value. Our engineering teams at Aha! are responsible for the entire life cycle of features. They gain a deeper understanding of the code they are building and why each piece is required. When features are divided among multiple teams handling multiple microservices, it is difficult for each engineer to understand the reasoning behind their requirements. Not only does this lower fulfillment from the engineers involved, but it also degrades the quality and allows for unexpected cases to arise from usage that some teams may not understand.
I have met very few engineers who enjoy a heavy process to make meaningful alterations in a system. More so, I meet engineers who take pride in finding sophisticated solutions that can improve an entire system. In our own code base, Kyle d’Oliveira was able to solve 90% of our N+1 queries across the entire application with a single code change.
Interrupt-driven
Our engineers don’t like meetings. Most of our teams do not have daily stand-ups. We pioneered The Responsive Method and part of that method is to be interrupt-driven. Our CTO Dr. Chris Waters likes to say we are “interrupt-driven, not meeting driven.” That is to say we do not schedule a meeting to tackle challenges that arise, but rather we interrupt our current work to handle the new priority immediately.
In a complex organization with simple microservices, interrupts can affect two, three, or many more teams. A meeting becomes a necessity to coordinate changes and dependencies between teams. Being interrupt driven is almost impossible when the complexity of the application has been distributed across services and teams.
The monolith gives our team the ability to limit interruptions to affect only a single engineer. Even large interruptions that require changes at multiple levels of the stack can typically be handled by a single engineer. And as Conway’s Law would suggest, structuring our organization around The Responsive Method has driven our architecture to remain a monolith.
Locking it down
We handle a lot of important data for our Aha! customers. Keeping their data secure is our top priority. We maintain an ISO27001 certification to ensure we are limiting any vulnerabilities in our system and process. When we build new features, security is paramount to all other concerns. We have thorough security reviews for every new endpoint, external dependency, major data migration, or any other code changes that have potential to open security vulnerabilities.
A monolith limits the scope in which we can introduce security issues. A single code base allows our security engineers to understand the potential risks in the application and to easily monitor all ingress points. If our code was distributed amongst numerous microservices with separate data concerns and endpoints, the scope and quantity of security considerations would multiply. Each new microservice would need to be analyzed and audited to verify we are not opening our customer's data up to new vulnerabilities from reparsing and reprocessing data in an inconsistent way.
Our Rails monolith also benefits from being written in consistent languages — Ruby and JavaScript. Aha! security engineers don’t need to be able to diagnose vulnerabilities in Python, Go, and any other language a team decides to create a service in. Instead, our engineers can focus on the Rails code and grow a deep knowledge of how to secure the environment for our users.
Evolve to resolve
There is no silver bullet in software architecture that will work for every team and application. For our team, the Rails monolith has so far met the needs of our engineers and our products. It has been described as the “least-worst architecture,” which is often the best option in computer science.
While keeping complexity in the code has many benefits we enjoy, it also comes with challenges. We encounter more of these challenges as we grow and have to evolve to resolve them.
Some of those evolutions even start to look a little different than a classic monolith. We are updating our background job processing by introducing event streaming with Kafka to supplement Resque background jobs. While this code runs on completely different physical architecture, it is still written to utilize the Rails monolith in the producers and consumers so we still reap the benefits of a single repository of shared code.
There is a very high level of code reuse in a monolith. While this is very beneficial in quickly building features, it actually comes as a cost to maintaining quality. It is imperative that teams using a monolith have test suites that cover their vital business functionality. Without proper test coverage, it is difficult for engineers to have confidence making changes to large monolithic systems.
A larger test suite comes with its own problems. As we covered earlier, Aha! engineers like to move fast and be agile enough to handle interruptions when they occur. Interruptions must be handled quickly in order for the responsive method to thrive. Each time we push a change, we are pushing the entire application with all functionality that exists in Aha! This means that as our test suite grows, it begins to limit how quickly we can push changes to production. This is why we built our pipeline so a deployment can quickly be rolled back to a previously stable version. We also generate blue/green deployments and validate the health of servers before rolling traffic to the new code.
It is also noteworthy that code complexity over organizational complexity requires an engineer to deeply understand all the impacts of a change on related functionality. Each of our engineers may be asked to handle complex tasks that cut across the application. Engineers must analyze the changes they are making to the system and have confidence that they are not causing other problems. Our code base can be difficult to manage for new engineers so we typically hire experienced engineers who are already familiar with the technology we are using.
Future of the monolith
We will continue to evolve our monolith and make alterations to the architecture to enable our needs. You can follow along here as we grow and expand our product suite and our monolith.
We don’t see any future where dismantling our monolith is a priority. Some of us have been on teams that spent months and even years converting monoliths into microservices. In the end these efforts usually produce systems that are tangled in dependencies and difficult for a single engineer to make meaningful changes within. It becomes difficult to drive changes on interruption and to quickly support customers across the stack.
For now, the monolith provides us with the tools to give our customers a complete product experience from lovable interactions to responsive support. Read along as we continue to share the journey over the next year and show ways that we are extending the life of our monolith.
We work fast at Aha! and we use our products to build our products. Our team is happy, productive, and hiring — join us!