Our Majestic Monolith: Part Two

We've covered the context in Part One. Now let's talk about the specifics of how we approach our Majestic Monolith.
Chris Young
Chris Young
December 07, 2022

In the previous post, we took a look at the contextual factors that led us to start with, and stick to, a monolithic architecture. We considered the project, the team and the underlying philosophy that has laid the foundation for this article.

Our Majestic Monolith

Harled is a Rails shop, and thus, our Majestic Monolith is based on a primary application written in the traditional Rails fashion, with a small number of microservices and cloud services augmenting where necessary. The following diagram offers a high-level overview of the architecture:

You'll notice that (to scale!) 99% of our offering is based on a Rails application. And when I say Rails application I mean a text-book rails app. We stick to the core of the framework and supplement with some tried and tested gems when necessary.

When it REALLY doesn't make sense to build a capability into the core application, we've opted to outsource to a few select microservices. This is usually the case when the number of dependencies or labour simply cannot be justified. An example would be a service that converts HTML to PDFs. This is done incredible well by Puppeteer, which is a node app and runs easily under a thin express shim.

We're Really Good at Rails

Given that we are such a small team, with such a large mission it is essential that we lean into our strengths. We're really good at Rails and thus the more time we spend there the more time we're at our best. This isn't to say that we can't venture and learn, but we're going to do a better job writing nice clean code than we are going to design, architect, deploy and maintain a kubernetes cluster or grafana server.

This might seem a little silly, but it is huge. By focusing on Rails, we consolidate (or compress as DHH would say), conversations, invention and problem solving to the smaller domain of Ruby, Ruby on Rails and the associated gems. It gives us a great depth in our experience while reducing surface area.

Something that we like about Rails specifically is the amount of opinionation included. Convention over configuration means that somebody else already went through the bike shedding process and saved us a lot of time.

We're Good Enough at Everything

As for everything else related to the offering, well, we're strategically just good enough. That is, sufficient to let the Rails app shine and not have things crumble around the monolith. Being developers and engineers, this is an incredibly difficult thing to admit and persist! There is nothing more tempting than to dive into the latest features of our cloud provider, or some new docker image that delivers amazing insights or to start horizontally scaling for the sake of horizontal scaling.

We've avoided learning a lot of things by keeping a couple of things really simple. Some things we've kept as simple as possible:

  1. Our development workflow
  2. Dev/prod parity
  3. Keeping our pipeline simple and fast
  4. A single repository (99.99% of the time the team is in our main repo)
  5. Our cloud runtime architecture
  6. Aggressive refactoring to standardize (for us "one way" has always beat many "best ways")

We've literally done just enough of these areas to get them working, moderately understood by the broader team and then allowed to run without constant poking and prodding. We've stayed away from the following:

  • Microservices
  • A/B testing
  • Dynamic scaling
  • Canary testing
  • Blue / Green deploys
  • Feature flags
  • ...

Yes, they are cool. Yes, the blog posts lure us. And no, we literally just don't have an acute need nor the required bandwidth to chase them. We're also cognizant of the opportunity cost associated with learning them (less time with Rails).

No Custom Application Experience / No Cloud Experience

Another important aspect in selecting a monolith is considering the organization that is receiving the application. In this case, the receiving organization does not have a robust / mature IT capability. They have very little experience in custom application development and no experience with cloud and cloud-native applications. The fewer infrastructure requirements the better.

Limiting our fronts for an organization like this is an absolute must. To try and "invent" offering management, design, customer success, DevOps / CloudOps and Security capabilities in one go is not something a team of 5 can do (100 .. maybe?).

Keeping our app complexity contained to one runtime greatly reduced our ask and footprint in any hosting environment. It also made it really easy to communicate to people who would feel very uncomfortable with some more en vogue architectures. For example, we have an application that talks to a database. That's it! Even if the organization is 20 years behind, that pattern is well understood.

Supporting Experimentation

Our Monolith (and Ruby on Rails), also enables us to experiment incredibly effectively. We have built, changed and removed a large number of features faster than the traditional user stories could have been written, vetted and designed. This super fast feedback loop hasn't been without its downsides, but it has also been the main reason for our success. The ability to consistently and rapidly respond to the needs of users in both incremental additions and net new capabilities.

For the net new capabilities, Rails scaffolding gets us a long way, and the ability to share common services from the monolith also gives any new mini-app super powers. For example, the following services come for free once you run in the monolith:

  • Access
  • Authentication
  • Authorization (RBAC, permissions, auditing)
  • Notifications
  • Theme / templates
  • Data transformation services
  • Events / telemetry

Yes, all of the above could be offered as microservices, with language specific SDKs and separate deployment and operations workflows. It would just require 10x the staff and 10x the knowledge and experience.

Given our constraints, a monolith has allowed us to build out some different features that traditionally might be seen as separate apps. However, building as a monolith first of all made it possible (the organization would have never made it off the starting line) and was able to instantly leverage the core aspects of the monolith in a production runtime sense. Compare that to a library of SDKs / or microservices where they still need to be integrated and configured to work properly. Our monolith offers a strong foundation with new features (or mini-apps) able to be released with very incremental development effort and no operational effort.

Would We Do it Again?

Absolutely. Mostly because there was no other version of this project, with this team, led by our philosophy, that would have been anywhere near as successful or effective as what we have done. In this scenario, the majestic monolith has grown the project from its first controller to its 142nd. I also believe it will serve the project well into the future and maintain course as the right choice given all things considered.

So, if you have also elected to employ a majestic monolith for the right reasons, don't be shy about it, celebrate it! We love the benefits we get from our monolith, and we're aware of how to manage some of the complexities.

Looking to join the effort and help us evolve our Majestic Monolith? If so, take a moment to review our open positions!

We hope you've found this post helpful! If you have any feedback please reach out on X and we would be happy to chat. 🙏

About the author

Chris Young

Chris is dedicated to driving meaningful change in the world through software. He has taken dozens of projects from napkin to production in fast yet measured way. Chris has experience delivering solutions to clients spanning fortune 100, not-for-profit and Government.