October 23, 2020; in Designing a Monolith Series

Designing a Monolith: Understanding Code Architecture (Part One)

Designing a Monolith: Understanding Code Architecture (Part One)

When web developers hear the word architecture, more often than not they first think of a server setup.

Server architecture example
Image courtesy of nginx.com

In this diagram, each component is separate as they have separate roles:

  • Load balancers
  • Front-facing web servers
  • Application servers
  • Database servers
  • In-memory data stores

As your application scales, these services be scaled independently of each other. Your application may have a larger database instance and a smaller Redis instance. It may have multiple Redis instances for different components of the application that scale differently than the rest of the application as well.

How does this relate to your application?

Out of the box, Rails treats its each part of the MVC stack the same way, providing you with a base class to inherit from:

  • ActiveRecord::Base has ApplicationRecord and lives in app/models
  • ActionController::Base has ApplicationController and lives in app/controllers
  • ActionMailer::Base has ApplicationMailer and lives in app/models
  • ActiveJob::Base has ApplicationJob and lives in app/jobs

Rails does a really good job at providing these conventions out of the box. They are pieces of architecture as they will be long-lived bits of core code. Without them, you lose a core feature:

  • Without ActiveRecord::Base, you lose ActiveRecord and its functionality
  • Without ActionController::Base, you lose ActionController and its functionality
  • Without ActionMailer::Base, you lose ApplicationMailer and its functionality
  • Without ActiveJob::Base, you lose ActiveJob and its functionality

Rails sets up this composition for you, assuming that this base class will contain common functionality used throughout each subclass:

  • ActiveRecord assumes that an application will have validations on most of its subclasses, so it exposes validates_*
  • Devise hooks into ActionController and gives you things like user_signed_in? and current_user
  • ActionMailer exposes things like default_url_options

This method of composition is a critical part of developing architecture for your application.

As your application scales, it develops its own patterns. Some of the common ones in Rails applications include decorators for handling view logic, serializers for building objects used directly in your response body, and a service layer for handling extraneous business logic. Rails makes it easy for a developer to put this logic in our app folder and have it automatically be available to the application provided it is named according to its expected naming conventions.

Because we give special treatment to each component of our code architecture with its own folder, why don’t we do the same with core features that lay on top of our traditional MVC stack?

I bet you already do this in at least one feature of your application: the admin dashboard.

If you’re Spotify, this is can be:

  • Artist Dashboard
  • User Account Interface

The Artist Dashboard is always going to call current_artist. The User Account Interface is always going to call current_user. These features have no reason to share a namespace.

For some reason, this doesn’t sit well with me:

class ArtistsSettingsController < ApplicationController;
class UsersSettingsController < ApplicationController;

Instead, I prefer:

module ArtistDashboard
  class SettingsController < ApplicationController; end

module UserDashboard
  class SettingsController < ApplicationController; end

Or even better:

module ArtistDashboard
  class BaseController < ApplicationController; end 
  class SettingsController < BaseController; end

module UserDashboard
  class BaseController < ApplicationController; end 
  class SettingsController < BaseController; end

This doesn’t stop at controllers, either. This is applied to each component of these features that are specific to just them: the mdoels, the mailers, the serializers, the objects in the service layer, all the concerns and everything in between.

The idea is that if one day we need to remove this feature, it’s a clean rm -rf and some manual cleanup instead of hunting down files.

As a developer, this reduces cognitive overhead because all the important parts of these individual features live next to each other. If they don’t, it’s a core concept like a User. It encourages separation of concerns via directory structure and it’s cost is a mere mkdir.

Thinking about features in terms of pieces of architecture makes an application easier to maintain, iterate upon, and test. While it requires prior preparation, the cost of doing so is low in comparison to long-term sustainability.

In part two, I’ll start deconstructing Spotify and implementing it as if it were a Rails application, following the same ideas as discussed here.