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.
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
hasApplicationRecord
and lives inapp/models
ActionController::Base
hasApplicationController
and lives inapp/controllers
ActionMailer::Base
hasApplicationMailer
and lives inapp/models
ActiveJob::Base
hasApplicationJob
and lives inapp/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 loseActiveRecord
and its functionality - Without
ActionController::Base
, you loseActionController
and its functionality - Without
ActionMailer::Base
, you loseApplicationMailer
and its functionality - Without
ActiveJob::Base
, you loseActiveJob
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 exposesvalidates_*
- Devise hooks into
ActionController
and gives you things likeuser_signed_in?
andcurrent_user
ActionMailer
exposes things likedefault_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
end
module UserDashboard
class SettingsController < ApplicationController; end
end
Or even better:
module ArtistDashboard
class BaseController < ApplicationController; end
class SettingsController < BaseController; end
end
module UserDashboard
class BaseController < ApplicationController; end
class SettingsController < BaseController; end
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.