A relatively brief discussion of different ways to structure a large and ever-growing web application for long-term sustainability.

This isn’t meant to be a comprehensive comparison of the solutions discussed, nor is it a one-size-fits-all solution to applications architecture. The main purpose is for me to work through some options.

tl;dr: microservices are great if you need them and have the resource

The context here is:

  • a small development team (2 developers, no dedicated ops people)
  • one already-existing medium-large Ruby on Rails app1
  • several “orbiting” smaller applications and services
  • new requirements (🎵 our work is never done, my children my children 🎵)

What I want to explore is how to improve the architecture of this application from “monolithic ball of mud” to something more maintainable.

Diagram with two axes for number of deployment units and modularity comparing monoliths, modular monoliths, microservices and distributed balls of mud
Various types of application compared by number of units and modularity; via Simon Brown

A Tale of Two Architectures

From the diagram above, and in line with the requirement for high cohesion there are two desirable architectures to choose from:

What’s the Difference?

The plan is to evaluate the pros and cons of each choice, and to select one based on the context of $DAYJOB. Your mileage will vary.

Modular Monolith

Pros

  • when you do have cross-cutting concerns, it’s much easier to create an interface/contract
  • substantially lower operational complexity compared to SOA
  • lower chance of ending up with a varying set of paradigms and tools between services

Cons

  • potentially harder to onboard new people (large apps are harder to reason about)
  • increased upgrading complexity
  • easy to build brittle/tightly coupled systems

Service-Oriented Architecture

Pros

  • services are relatively small and easy to reason about
  • better opportunities for scaling
  • for larger teams, much lower chance for conflict when making changes

Cons

  • a complex distributed system is significantly harder to reason about and debug
  • substantially higher operational complexity
  • still easy to build brittle/tightly coupled systems or a distributed monolith2

Similarities

These two options do share some similarities when used properly:

  • high cohesion
  • loose coupling
  • bounded context
  • encapsulation

The implementation of these features varies, and how strictly they are enforced by the technology varies, but in principle, the two offerings provide many of the same benefits.

Does it Matter?

Only insofar as we need to do a bit of design. Not too much, but not so little that the application rots.

I was really just looking for an excuse to include this:

Chart showing time against cumulative functionality which indicates there is a point at which too much design stops paying off, but too little will cause the codebase to deteriorate
don't overdesign or underdesign; see article for an in-depth explanation

What Should I Do?

Let’s take a look at that context from earlier again:

Small Development Team
No expectation of many parts of the system being worked on at once
Everyone works on everything
One Large Monolith
Not currently very modular
Too large to reason about as a whole
Several Smaller Applications in Play
Continue to split these out when appropriate
Come up with robust guidelines for when this should happen
New Requirements
Need to be accommodated, usually faster than any rearchitecting would allow
Need to be well-contained so they don’t end up in the blast-radius of other services

With all of this in mind, I’m leaning towards the modular monolith for the following reasons:

  1. the additional short-term and ongoing complexity of managing a suite of distributed services is not achievable with the current size of the team, and any long-term complexity benefits of SOA cannot be bourne by a team this size

  2. the additional operational complexity is not achievable without additional specific ops or devops resource

  3. the large-app onboarding problem can be partially solved using sufficiently detailed documentation3

  4. much of our business-critical data is quite tightly coupled and interdependent due to the nature of the business; writing course-grained APIs to make this performant could potentially be quite cumbersome

  5. the cost of accidentally building a distributed monolith is too terrifying to even consider

  6. the resource required to upgrade a rails app of this scale feels (I have no data, just my own experience) less than maintaining a complex distributed system

The following criteria being met is a good indicator that a given product/service should not be a module within the monolith:

  1. there is very little data coupling, or it’s mostly unidirectional

  2. the service requires crossing some security boundary

  3. the service has a different paradigm to the main application

Further Reading

or, I stand on the shoulders of giants

Footnotes

  1. 120 controllers, 150 models, 60 service classes, approaching 100kloc (including ruby, javascript, erb) 

  2. this is basically the worst possible outcome 

  3. for example, using the C4 Model